Repository navigation
Conversation
…at is marked to close uws_res_end_without_body marked the response done and left the uncork and the close gate to whoever held the socket's cork. Every other end runs both itself. A bodyless end (a 304, a HEAD response, the bodyless error responses) that completed outside the read of its request could therefore leave a connection open that `Connection: close`, HTTP/1.0 or a graceful server.stop() had marked to close: when another socket's cork() flushed its cork slot first, and whenever the caller ran no gate of its own (the 500 of an HTML import whose build failed, the 405 of the dev server). The shim now completes through HttpResponse::finishResponseInCorkBuffer(), the uncork and close gate that internalEnd() ends on. A response that JavaScript produced is sent when it completes, with or without a body. node:http res.destroy() ended its response through the same shim. It drops what is still corked and closes the socket itself, so it gets uws_res_abandon, which does what the shim did before.
Collaborator
Author
|
Updated 11:44 AM PT - Nov 10th, 2026
✅ @robobun, your commit 76f3a768876351845b2f157143c85dfb2771e9bf passed in 🧪 To try this PR locally: bunx bun-pr 48486That installs a local version of the PR into your bun-48486 --bun |
Collaborator
Author
|
Status How it was reproduced, on
With this PR each connection closes right after the response. The tests are in |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Connection: close, HTTP/1.0 orserver.stop(). The client gets no FIN untilidleTimeout.uws_res_end_without_body(src/uws_sys/libuwsockets.cpp) marks the response done and runs no uncork and no close gate.HttpResponse::cork()skips its own gate when another socket'scork()flushed the response's slot (HttpResponse.h:968), and callers outsidecork()have none.Fix
finishResponseInCorkBuffer(0), the uncork and close gateinternalEnd()ends on.res.destroy()drops corked bytes and closes the socket itself. It moves touws_res_abandon, the shim's old behaviour.bun-server.test.ts(7 cases),bun-serve-html.test.ts(2),serve.test.ts,node-http-req-complete.test.ts. Suites are in Notes.Background
cork()flushes the oldest.cork()'s lost-slot return (uws: run the close gate in HttpResponse::cork() when the cork slot was lost #37446): every post-awaitresponse with a body exits there, and ends outsidecork()stay ungated.Downsides
uws_res_end_without_bodycan close the socket before it returns, asend()does. http: consolidate the open fetch, Bun.serve, node:http, node:http2 and HTTP/3 pull requests #46528 keeps the shim's "No close gate here" note..textof the uWS unit: +700 bytes.Notes
Reported by a maintainer in review of #37074: "
HttpResponse::cork()early return skips the close gate ... a completedConnection: close/ HTTP/1.0 response on a non-parsing socket is never shut".Repro of the reported order. An async handler answers a
Connection: closePOST whose body never arrives:The client receives
HTTP/1.1 304 Not Modifiedand no FIN. Ending the response rejects the unread body, and the microtasks that run then execute while the response still holds its cork slot. Nocork()call is needed: four such requests that complete in one microtask drain leave two connections open, because each response completes inside the drain of the one before it.Callers outside
cork(). The request that waited for the bundle of an HTML import is answered from an event-loop task. After500 Build Failed(production build failed) and after the dev server's 405, the client gets no FIN on main, with no other socket involved.Also in the diff
RequestContext::end_without_bodyno longer callsclose_if_done_and_marked(). That shim stays for the sendfile end.HTMLBundle.rsanddev_server/deferred_request.rsthat said this end cannot close the socket are gone.Why not the gate at
cork()'s lost-slot returnawaitends throughinternalEnd(), which uncorks.cork()then takes the lost-slot return. A gate there runs for every such response.wsrewritten on native transports (consolidates the open WebSocket PRs) #45223server.upgrade()runs insideresp.corkedand uWS adopts the socket in place. At that returnthiscan be a live WebSocket, so uws: run the close gate in HttpResponse::cork() when the cork slot was lost #37446 as written would read a WebSocket as anHttpResponseData.cork().Question for a maintainer. Is it acceptable that
uws_res_end_without_bodycloses the socket before it returns? The note said callers keep using the response. Today the callers either return or callclear_aborted/clear_on_writable/clear_timeout, which are no-ops on a closed socket becauseend()andtryEnd()already close in-call. If the end has to stay non-closing, the alternative is to makeuws_res_close_if_done_and_markeduncork (as #41341 does) and to add a gate call at each raw end outside the parser.Tests
bun-server.test.ts, "a response without a body closes a connection that is marked to close": requestConnection: close, HTTP/1.0, responseConnection: close,server.stop()in flight, TLS, and the two eviction orders (two corked WebSockets, two streaming responses). All 7 time out on main.bun-serve-html.test.ts: the two HTML import cases above. Both time out on main.serve.test.ts, "share one send() until a handler runs JavaScript": new rows for responses without a body. On mainmixedHeadis 3 segments.fetchHead8,staticHead1,fetch3048 andstatic3041 are the same before and after.node-http-req-complete.test.ts, "res.destroy() drops the chunk that res.write() queued in the same turn": passes on main, under Node v26.3.0 and with this change. It fails (the client receives the dropped chunk) whenuws_res_abandonis made to gate, and no existing test did.Measurements
llvm-objdump -dofsrc/uws_sys/libuwsockets.cppcompiled with the release flags and without LTO, TCP arm, overuws_res_end_without_body,finishResponseInCorkBufferanduncorkCompletedResponse(calls to the one-line usockets accessors not included: 7 on main, 14 here). The 110 is what an end with a body already pays at completion.perf,valgrind,straceandbloatyare not installed in the build container, so there is no dynamic count and no pair of full release builds.HttpResponse::cork(),internalEnd(),uws_res_end,uws_res_try_end,uws_res_end_stream,uws_res_corkandHttpContext::onData.uws_res_end_without_bodygoes from 450 to 264 bytes. New:uws_res_abandon(488 bytes) and two out-of-linefinishResponseInCorkBufferinstantiations (199 bytes each, which theNodeHTTP.cppunit already has).getsockopt(TCP_INFO)in the test above.Suites run on the debug build:
bun-server,serve,bun-serve-html,bun-serve-static,bun-serve-file,bun-serve-routes,serve-directory-routes,request-smuggling,websocket-server,node-http,node-http-req-complete,node-http-server-abort-events,node-http-closed-response,node-http-server-close-drain,node-http-upgrade-body,test/bake/dev/html,test/bake/dev/plugins,test/bake/serve-plugins-dev-server. Failures that also occur without this change: the privileged port test (the container runs as root), one stack-shape assertion innode-http-closed-responseon debug builds, and timeouts of subprocess-heavy tests under the debug build that pass alone.Not changed here
uws_res_end_sendfilestill completes without gating and keeps its caller-sideuws_res_close_if_done_and_marked.cork()'s tail and the one atHttpContext::onData's tail stay as they are.uws_res_abandonkeeps the header writes the shim made, sores.destroy()puts the same bytes on the wire as on main. node:http: send nothing on destroy() before the headers go out, and a 500 (not an empty 200) for a throwing listener #39366 and http: consolidate the open fetch, Bun.serve, node:http, node:http2 and HTTP/3 pull requests #46528 each add auws_res_abandonfor the same caller that drops them. If one of them lands first, this PR takes that shim and changes onlyuws_res_end_without_body.Earlier attempts: #35555 (the end shims gate themselves) and #37446 (gate at
cork()'s tail, the source of the two eviction tests).