Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions examples/Crc32.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,23 +44,23 @@ int main() {
std::cout << key << ": " << value << std::endl;
}

auto isAborted = std::make_shared<bool>(false);
uint32_t crc = 0xFFFFFFFF;
res->onData([res, isAborted, crc](std::string_view chunk, bool isFin) mutable {
res->onData([crc](auto *res, std::string_view chunk, uint64_t maxRemainingBodyLength) mutable {
/* Aborted, there is no response anymore */

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fact res comes as argument is objectively better. It requires less capture space (less likely to spill over as malloc), and passing nullptr or nullopt makes an excellent indication for "aborted". And since aborted comes from within here, anything that async gathers data do not need to capture the shared_ptr aborted.

Only thing is, the name should be onData and be an overload here in C++. I am still thinking about the exact details (for instance do we want to pass std::optional to fully mark that there can come nullopt. That's probably the modern safer way.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, if the "base" function onData that takes CB of arity 3 is called with nullopt on abort, we don't even need to overwrite the onAborted callback, we can just change the behavior so that the lib calls into onData on aborts (as well as onAborted). That makes is fully backwards compatible, if we then implement the old onData so that it skips calls that have nullopt.

The only thing to think about now is the onData("", 0) that is emitted on GET having no body. That thing needs some thinking because it still needs to be backwards compatible. So it probably still need that behavior.

Hmm..

if (!res) {
return;
}

if (chunk.length()) {
crc = crc32(chunk.data(), chunk.length(), crc);
}

if (isFin && !*isAborted) {
if (maxRemainingBodyLength == 0) {
std::stringstream s;
s << std::hex << (~crc) << std::endl;
res->end(s.str());
}
});

res->onAborted([isAborted]() {
*isAborted = true;
});
}).listen(3000, [](auto *listen_socket) {
if (listen_socket) {
std::cerr << "Listening on port " << 3000 << std::endl;
Expand Down
15 changes: 8 additions & 7 deletions fuzzing/EpollHelloWorld.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,15 @@ void test() {
res->end("Hello world!");
}
}).post("/*", [](auto *res, auto *req) {
res->onAborted([]() {
/* We might as well use this opportunity to stress the loop a bit */
uWS::Loop::get()->defer([]() {
res->onData([](auto *res, std::string_view chunk, uint64_t maxRemainingBodyLength) {
if (!res) {
/* We might as well use this opportunity to stress the loop a bit */
uWS::Loop::get()->defer([]() {

});
});
res->onData([res](std::string_view chunk, bool isEnd) {
if (isEnd) {
});
return;
}
if (maxRemainingBodyLength == 0) {
res->cork([res, chunk]() {
res->write("something ahead");
res->end(chunk);
Expand Down
2 changes: 1 addition & 1 deletion misc/READMORE.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ uWS::App().get("/hello", [](auto *res, auto *req) {

Important for all routes is that "req", the `uWS::HttpRequest *` dies with return. In other words, req is stack allocated so don't keep it in your pocket.

res, the `uWS::HttpResponse<SSL> *` will be alive and accessible until either its .onAborted callback emits, or you've responded to the request via res.end or res.tryEnd.
res, the `uWS::HttpResponse<SSL> *` will be alive and accessible until either its .onAborted callback emits, or you've responded to the request via res.end or res.tryEnd. Instead of .onAborted you can use the .onData and .onWritable overloads that take the response as first argument: they get the abort as a null response.

In other words, you either respond to the request immediately and return, or you attach lambdas to the res (which may hold captured data), and respond later on in some other async callback.

Expand Down
34 changes: 26 additions & 8 deletions src/HttpContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,17 @@ struct HttpContext {
httpResponseData->onAborted();
}

/* Also to the onData and onWritable overloads that get the response, in this order. markDone removes them,
* so they are only set while pending */
MoveOnlyFunction<void(HttpResponse<SSL> *, std::string_view, uint64_t)> inStreamWithAbort = std::move(httpResponseData->inStreamWithAbort);
MoveOnlyFunction<bool(HttpResponse<SSL> *, uintmax_t)> onWritableWithAbort = std::move(httpResponseData->onWritableWithAbort);
if (inStreamWithAbort) {
inStreamWithAbort(nullptr, {}, 0);
}
if (onWritableWithAbort) {
onWritableWithAbort(nullptr, 0);
}

/* Destruct socket ext */
httpResponseData->~HttpResponseData<SSL>();

Expand Down Expand Up @@ -212,8 +223,9 @@ struct HttpContext {
return nullptr;
}

/* Returning from a request handler without responding or attaching an onAborted handler is ill-use */
if (!((HttpResponse<SSL> *) s)->hasResponded() && !httpResponseData->onAborted) {
/* Returning from a request handler without responding or attaching an onAborted handler is ill-use.
* The onData and onWritable overloads that get the response also get the abort */
if (!((HttpResponse<SSL> *) s)->hasResponded() && !httpResponseData->onAborted && !httpResponseData->inStreamWithAbort && !httpResponseData->onWritableWithAbort) {
/* Throw exception here? */
std::cerr << "Error: Returning from a request handler without responding or attaching an abort handler is forbidden!"
<< std::endl
Expand All @@ -223,7 +235,7 @@ struct HttpContext {
}

/* If we have not responded and we have a data handler, we need to timeout to enfore client sending the data */
if (!((HttpResponse<SSL> *) s)->hasResponded() && httpResponseData->inStream) {
if (!((HttpResponse<SSL> *) s)->hasResponded() && (httpResponseData->inStream || httpResponseData->inStreamWithAbort)) {
us_socket_timeout(SSL, (us_socket_t *) s, HTTP_IDLE_TIMEOUT_S);
}

Expand All @@ -232,7 +244,7 @@ struct HttpContext {

}, [httpResponseData](void *user, std::string_view data, uint64_t maxRemainingBodyLength) -> void * {
/* We always get an empty chunk even if there is no data */
if (httpResponseData->inStream) {
if (httpResponseData->inStream || httpResponseData->inStreamWithAbort) {

/* Todo: can this handle timeout for non-post as well? */
if (maxRemainingBodyLength == 0) {
Expand All @@ -249,7 +261,11 @@ struct HttpContext {
}

/* We might respond in the handler, so do not change timeout after this */
httpResponseData->inStream(data, maxRemainingBodyLength);
if (httpResponseData->inStream) {
httpResponseData->inStream(data, maxRemainingBodyLength);
} else {
httpResponseData->callInStreamWithAbort((HttpResponse<SSL> *) user, data, maxRemainingBodyLength);
}

/* Was the socket closed? */
if (us_socket_is_closed(SSL, (struct us_socket_t *) user)) {
Expand All @@ -262,7 +278,8 @@ struct HttpContext {
}

/* If we were given the last data chunk, reset data handler to ensure following
* requests on the same socket won't trigger any previously registered behavior */
* requests on the same socket won't trigger any previously registered behavior.
* The onData that gets the response stays until the response ends, markDone removes it */
if (maxRemainingBodyLength == 0) {
httpResponseData->inStream = nullptr;
}
Expand Down Expand Up @@ -347,13 +364,14 @@ struct HttpContext {
HttpResponseData<SSL> *httpResponseData = (HttpResponseData<SSL> *) asyncSocket->getAsyncSocketData();

/* Ask the developer to write data and return success (true) or failure (false), OR skip sending anything and return success (true). */
if (httpResponseData->onWritable) {
if (httpResponseData->onWritable || httpResponseData->onWritableWithAbort) {
/* We are now writable, so hang timeout again, the user does not have to do anything so we should hang until end or tryEnd rearms timeout */
us_socket_timeout(SSL, s, 0);

/* We expect the developer to return whether or not write was successful (true).
* If write was never called, the developer should still return true so that we may drain. */
bool success = httpResponseData->callOnWritable(httpResponseData->offset);
bool success = httpResponseData->onWritable ? httpResponseData->callOnWritable(httpResponseData->offset)
: httpResponseData->callOnWritableWithAbort((HttpResponse<SSL> *) s, httpResponseData->offset);

/* The developer indicated that their onWritable failed. */
if (!success) {
Expand Down
54 changes: 49 additions & 5 deletions src/HttpResponse.h
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ struct HttpResponse : public AsyncSocket<SSL> {
Super::timeout(HTTP_TIMEOUT_S);
}

/* Remove onAborted, onWritable function and mark done if we reach the end, or if we were given no data (faked size like in HEAD response) */
/* Remove onAborted, onWritable, the onData that gets the response and mark done if we reach the end, or if we were given no data (faked size like in HEAD response) */
/* I need to figure out if this line should rather be simply httpResponseData->offset == data.length() */
/* No that can't be right, tryEnd with fake length should not complete the response even if the smaller chunk wrote in one go */
/* Possibly need to separate endWithoutBody and tryEnd with fake length into two separate calls with a boolean that explicitly marks isHeadOnly */
Expand Down Expand Up @@ -589,23 +589,47 @@ struct HttpResponse : public AsyncSocket<SSL> {
return this;
}

/* Attach handler for writable HTTP response */
/* Attach handler for writable HTTP response.
* Deprecated: the overload that gets the response also gets the abort */
HttpResponse *onWritable(MoveOnlyFunction<bool(uintmax_t)> &&handler) {
HttpResponseData<SSL> *httpResponseData = getHttpResponseData();

httpResponseData->onWritable = std::move(handler);
httpResponseData->onWritableWithAbort = nullptr;
return this;
}

/* Attach handler for aborted HTTP request */
/* Attach handler for writable HTTP response, called with the response and the offset. If the request is aborted
* before the response ends, it is called once with a null response (what it returns is then ignored), so no
* onAborted is needed. With the onData that gets the response too, both get the abort, onData first. */
HttpResponse *onWritable(MoveOnlyFunction<bool(HttpResponse *, uintmax_t)> &&handler) {
HttpResponseData<SSL> *httpResponseData = getHttpResponseData();

httpResponseData->onWritableWithAbort = std::move(handler);
httpResponseData->onWritable = nullptr;
return this;
}

/* Remove the writable handler, of either kind */
HttpResponse *onWritable(std::nullptr_t) {
HttpResponseData<SSL> *httpResponseData = getHttpResponseData();

httpResponseData->onWritable = nullptr;
httpResponseData->onWritableWithAbort = nullptr;
return this;
}

/* Attach handler for aborted HTTP request.
* Deprecated: the onData and onWritable overloads that get the response also get the abort */
HttpResponse *onAborted(MoveOnlyFunction<void()> &&handler) {
HttpResponseData<SSL> *httpResponseData = getHttpResponseData();

httpResponseData->onAborted = std::move(handler);
return this;
}

/* Attach a read handler for data sent. Will be called with FIN set true if last segment. */
/* Attach a read handler for data sent. Will be called with FIN set true if last segment.
* Deprecated: the overload that gets the response also gets the abort */
void onData(MoveOnlyFunction<void(std::string_view, bool)> &&handler) {
if (handler) {
onDataV2([handler = std::move(handler)](std::string_view chunk, uint64_t maxRemainingBodyLength) mutable {
Expand All @@ -616,10 +640,30 @@ struct HttpResponse : public AsyncSocket<SSL> {
}
}

/* Attach a read handler for data sent. Will be called with maxRemainingBodyLength. maxRemainingBodyLength == 0 is the same as isLast. */
/* Attach a read handler for data sent. Will be called with the response, the chunk and maxRemainingBodyLength,
* maxRemainingBodyLength == 0 is the same as isLast. If the request is aborted before the response ends, also
* after the last chunk, it is called once with a null response, so no onAborted is needed. After the response
* ends it gets nothing more. */
void onData(MoveOnlyFunction<void(HttpResponse *, std::string_view, uint64_t)> &&handler) {
HttpResponseData<SSL> *data = getHttpResponseData();
data->inStreamWithAbort = std::move(handler);
data->inStream = nullptr;

/* Always reset this counter here */
data->received_bytes_per_timeout = 0;
}

/* Remove the read handler, of either kind */
void onData(std::nullptr_t) {
onDataV2(nullptr);
}

/* Attach a read handler for data sent. Will be called with maxRemainingBodyLength. maxRemainingBodyLength == 0 is the same as isLast.
* Deprecated: the onData overload that gets the response also gets the abort */
void onDataV2(MoveOnlyFunction<void(std::string_view, uint64_t)> &&handler) {
HttpResponseData<SSL> *data = getHttpResponseData();
data->inStream = std::move(handler);
data->inStreamWithAbort = nullptr;

/* Always reset this counter here */
data->received_bytes_per_timeout = 0;
Expand Down
62 changes: 45 additions & 17 deletions src/HttpResponseData.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,12 @@

#include "MoveOnlyFunction.h"

#include <type_traits>

namespace uWS {

template <bool> struct HttpResponse;

template <bool SSL>
struct HttpResponseData : AsyncSocketData<SSL>, HttpParser {
template <bool> friend struct HttpResponse;
Expand All @@ -38,6 +42,9 @@ struct HttpResponseData : AsyncSocketData<SSL>, HttpParser {
onAborted = nullptr;
/* Also remove onWritable so that we do not emit when draining behind the scenes. */
onWritable = nullptr;
/* The handlers that get the abort get nothing more, not even the rest of the body */
onWritableWithAbort = nullptr;
inStreamWithAbort = nullptr;

/* We are done with this request */
state &= ~HttpResponseData<SSL>::HTTP_RESPONSE_PENDING;
Expand All @@ -46,8 +53,22 @@ struct HttpResponseData : AsyncSocketData<SSL>, HttpParser {
/* Caller of onWritable. It is possible onWritable calls markDone so we need to borrow it.
* It is also possible user code sets a new onWritable while running user registered onWritable. */
bool callOnWritable(uintmax_t offset) {
return callBorrowed(onWritable, offset);
}

/* Callers of the handlers that get the abort, borrowed for the same reasons */
bool callOnWritableWithAbort(HttpResponse<SSL> *res, uintmax_t offset) {
return callBorrowed(onWritableWithAbort, res, offset);
}

void callInStreamWithAbort(HttpResponse<SSL> *res, std::string_view data, uint64_t maxRemainingBodyLength) {
callBorrowed(inStreamWithAbort, res, data, maxRemainingBodyLength);
}
private:
template <typename R, typename... Args>
static R callBorrowed(MoveOnlyFunction<R(Args...)> &slot, Args... args) {
/* 1. Borrow the real callback */
MoveOnlyFunction<bool(uintmax_t)> borrowedOnWritable = std::move(onWritable);
MoveOnlyFunction<R(Args...)> borrowed = std::move(slot);

/* 2. Setup the stack-based detection flag */
bool placeholderReplaced = false;
Expand All @@ -72,25 +93,28 @@ struct HttpResponseData : AsyncSocketData<SSL>, HttpParser {
};

/* 3. Set placeholder with the captured Sentinel */
onWritable = [tracker = Sentinel(&placeholderReplaced)](uintmax_t) {
return true;
slot = [tracker = Sentinel(&placeholderReplaced)](Args...) -> R {
if constexpr (!std::is_void_v<R>) {
return true;
}
};

/* 4. Run the borrowed callback */
bool ret = borrowedOnWritable(offset);

/*
5. If placeholderReplaced is STILL false, it means the lambda (and its Sentinel)
is still sitting inside 'onWritable'. If it's true, the lambda was destroyed
to make room for a new one.
*/
if (!placeholderReplaced) {
onWritable = std::move(borrowedOnWritable);
}

return ret;
/* 4. Run the borrowed callback. 5. When done, if placeholderReplaced is STILL false, it means the lambda (and
its Sentinel) is still sitting inside the slot, so put the callback back. If it's true, the lambda was
destroyed to make room for a new one. */
struct PutBack {
MoveOnlyFunction<R(Args...)> &slot, &borrowed;
bool &placeholderReplaced;
~PutBack() {
if (!placeholderReplaced) {
slot = std::move(borrowed);
}
}
} putBack{slot, borrowed, placeholderReplaced};

return borrowed(args...);
}
private:

/* Bits of status */
enum {
HTTP_STATUS_CALLED = 1, // used
Expand All @@ -104,6 +128,10 @@ struct HttpResponseData : AsyncSocketData<SSL>, HttpParser {
MoveOnlyFunction<bool(uintmax_t)> onWritable;
MoveOnlyFunction<void()> onAborted;
MoveOnlyFunction<void(std::string_view, uint64_t)> inStream; // onData
/* The onWritable and onData overloads that get the response first, and the abort as a null response until
* markDone. Each one replaces the old handler of the same kind, and the other way around */
MoveOnlyFunction<bool(HttpResponse<SSL> *, uintmax_t)> onWritableWithAbort;
MoveOnlyFunction<void(HttpResponse<SSL> *, std::string_view, uint64_t)> inStreamWithAbort;
/* Outgoing offset */
uintmax_t offset = 0;

Expand Down
Loading
Loading