Skip to content
Closed
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
35 changes: 32 additions & 3 deletions packages/bun-uws/src/HttpContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,12 @@ struct HttpContext {
us_socket_unref(s);
return s;
}
/* Same for bytes that arrive behind a finished response that closes the
* connection while its body is still draining (onWritable closes then). */
if (httpResponseData->isDrainingBeforeClose() && !httpResponseData->isConnectRequest) [[unlikely]] {
us_socket_unref(s);
return s;
}
}

/* Cork this socket */
Expand Down Expand Up @@ -409,16 +415,29 @@ struct HttpContext {
auto *nodeHttpResponseData = (HttpResponseData<SSL, true> *) httpResponseData;
nodeHttpRequestTrailers = &nodeHttpResponseData->nodeHttpRequestTrailers;
}
/* node:http compat: set when the request handler below stops the parse at
* a request pipelined behind a response that closes the connection. */
bool stoppedBehindClosingResponse = false;

auto result = httpResponseData->template consumePostPadded<IsNodeHttp>(httpContextData->maxHeaderSize, httpResponseData->isConnectRequest, httpContextData->flags.requireHostHeader,httpContextData->flags.useStrictMethodValidation, httpContextData->flags.useInsecureHTTPParser, httpContextData->flags.useLenientTransferEncoding, nodeHttpRequestTrailers, &httpResponseData->chunkedExtensionsByteCount, data, (unsigned int) length, s, [httpContextData, &stoppedBehindClosingResponse](void *s, HttpRequest *httpRequest) -> void * {

auto result = httpResponseData->template consumePostPadded<IsNodeHttp>(httpContextData->maxHeaderSize, httpResponseData->isConnectRequest, httpContextData->flags.requireHostHeader,httpContextData->flags.useStrictMethodValidation, httpContextData->flags.useInsecureHTTPParser, httpContextData->flags.useLenientTransferEncoding, nodeHttpRequestTrailers, &httpResponseData->chunkedExtensionsByteCount, data, (unsigned int) length, s, [httpContextData](void *s, HttpRequest *httpRequest) -> void * {

HttpResponseData<SSL> *httpResponseData = (HttpResponseData<SSL> *) us_socket_ext((us_socket_t *) s);

/* node:http compat: never dispatch a request pipelined behind a
* response that closes the connection (still draining, or flushed by
* the uncork after this parse). Dispatching it would reset the
* response state, drop HTTP_CONNECTION_CLOSE and answer it behind a
* body the peer reads up to the FIN. */
if (IsNodeHttp && httpResponseData->isDrainingBeforeClose()) [[unlikely]] {
stoppedBehindClosingResponse = true;
return nullptr;
}

/* For every request we reset the timeout and hang until user makes action */
/* Warning: if we are in shutdown state, resetting the timer is a security issue! */
us_socket_timeout((us_socket_t *) s, 0);

HttpResponseData<SSL> *httpResponseData = (HttpResponseData<SSL> *) us_socket_ext((us_socket_t *) s);

/* node:http compat: the JS layer stopped HTTP processing on this
* connection (the user emitted 'close' on the socket - Node frees
* the parser there); abandon the rest of the buffer. */
Expand Down Expand Up @@ -734,6 +753,16 @@ struct HttpContext {
/* It is okay to uncork a closed socket and we need to */
((AsyncSocket<SSL> *) s)->uncork();

/* node:http compat: parsing stopped behind a response that closes the
* connection. If the uncork above flushed the last of it, close now;
* otherwise onWritable closes once the buffer drains. */
if constexpr (IsNodeHttp) {
if (stoppedBehindClosingResponse && !us_socket_is_closed(s)) {
us_socket_unref(s);
((HttpResponse<SSL> *) s)->closeIfDoneAndMarked(httpResponseData);
}
}

/* We cannot return nullptr to the underlying stack in any case */
return s;
}
Expand Down
5 changes: 5 additions & 0 deletions packages/bun-uws/src/HttpResponse.h
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,11 @@ struct HttpResponse : public AsyncSocket<SSL> {
/* This will be sent always when state is HTTP_WRITE_CALLED inside internalEnd, so no need to write the terminating 0 chunk here */
/* Super::write("\r\n0\r\n\r\n", 7); */

/* As in end(): the close is what terminates a close-delimited body. */
if (httpResponseData->state & HttpResponseData<SSL>::HTTP_CLOSE_DELIMITED) {
httpResponseData->state |= HttpResponseData<SSL>::HTTP_CONNECTION_CLOSE;
}

return internalEnd({nullptr, 0}, 0, false, false, closeConnection);
}

Expand Down
9 changes: 9 additions & 0 deletions packages/bun-uws/src/HttpResponseData.h
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,15 @@ struct HttpResponseData : AsyncSocketData<SSL>, HttpParser {
|| ((state & HTTP_NODE_RECEIVED_FIN) && nodeHttpQueuedPipelinedCount == 0)
|| ((state & HTTP_CLOSE_WHEN_IDLE) && this->isIdle);
}

/* The response that closes this connection (Connection: close, HTTP/1.0, a
* close-delimited body) is complete; the socket only stays open until its
* buffered bytes drain. Nothing received from here on is a request this
* connection may answer (RFC 9112 9.6), and after a close-delimited body the
* peer would read whatever we send as more body. */
bool isDrainingBeforeClose() const {
return (state & (HTTP_CONNECTION_CLOSE | HTTP_RESPONSE_PENDING)) == HTTP_CONNECTION_CLOSE;
}
};

/* Per-connection state that only node:http compat servers need.
Expand Down
23 changes: 18 additions & 5 deletions src/js/node/_http_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2274,20 +2274,32 @@ function renderNativeHeaders(res) {
// header is rendered so the advertised value matches the transport.
let closeDelimited = false;
let forceChunked = false;
// False for HTTP/1.0 (set by the constructor) or when the user cleared it.
const chunkedByDefault = !!res.useChunkedEncodingByDefault;
// Node's _storeHeader shouldSendKeepAlive: without chunked encoding only an
// explicit Content-Length lets the connection persist.
const canPersist = chunkedByDefault || storedContentLength !== undefined;
if (storedContentLength === undefined && storedTransferEncoding === undefined) {
if (res._hasBody === false) {
// HEAD / 204 / 304 / 1xx: there is no body to delimit, so removing the
// framing headers must not close the connection (Node's _storeHeader
// checks !_hasBody before its close-delimited else-branch).
} else if (!chunkedByDefault) {
// Node's _storeHeader `else if (!this.useChunkedEncodingByDefault) this._last = true`,
// taken before the Content-Length branch: no framing header at all, the body
// runs until the connection closes.
closeDelimited = true;
res[kMustCloseConnection] = true;
} else if (res._removedTE) {
closeDelimited = true;
res[kMustCloseConnection] = true;
} else if (res._removedContLen || res[kFramingFrozenChunked]) {
// Node's _storeHeader falls through to chunked only when useChunkedEncodingByDefault
// (false for HTTP/1.0); the native writer never chunk-frames HTTP/1.0, so the rest is
// close-delimited. An explicit writeHead() reaches the same null-_contentLength fallthrough.
// Node's _storeHeader falls through to chunked here. The flag is still set for an
// HTTP/1.0 request that sent `TE: chunked`, but the native writer never chunk-frames
// HTTP/1.0, so that stays close-delimited. An explicit writeHead() reaches the same
// null-_contentLength fallthrough.
const req = res.req;
if (res.useChunkedEncodingByDefault && req.httpVersionMajor >= 1 && req.httpVersionMinor >= 1) {
if (req.httpVersionMajor >= 1 && req.httpVersionMinor >= 1) {
forceChunked = true;
} else {
closeDelimited = true;
Expand All @@ -2307,6 +2319,7 @@ function renderNativeHeaders(res) {
if (
!defectiveNoBodyResponse &&
!closeDelimited &&
canPersist &&
!res.maxRequestsOnConnectionReached &&
res.shouldKeepAlive !== false &&
requestShouldKeepAlive(res.req)
Expand All @@ -2328,7 +2341,7 @@ function renderNativeHeaders(res) {
// Like Node's shouldSendKeepAlive/_last handling: a user-cleared
// shouldKeepAlive (graceful-shutdown helpers set it on in-flight
// responses) must also end the socket after 'finish'.
if (res.shouldKeepAlive === false) {
if (res.shouldKeepAlive === false || !canPersist) {
res[kMustCloseConnection] = true;
}
autoHeaders |= AUTO_HEADER_CONN_CLOSE;
Expand Down
Loading
Loading