-
Notifications
You must be signed in to change notification settings - Fork 5.1k
node:http: check the response again before upgrade() writes its headers #43028
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1673,15 +1673,13 @@ where | |
| // SAFETY: from_js returns a live *mut NodeHTTPResponse; shared — | ||
| // its mutable state is `Cell`/`JsCell` and `upgrade` takes `&self`. | ||
| let node_http_response = unsafe { &*node_http_response }; | ||
| if node_http_response | ||
| .flags | ||
| .get() | ||
| .contains(NodeHTTPResponseFlags::ENDED) | ||
| || node_http_response | ||
| let is_ended_or_closed = || { | ||
| node_http_response | ||
| .flags | ||
| .get() | ||
| .contains(NodeHTTPResponseFlags::SOCKET_CLOSED) | ||
| { | ||
| .intersects(NodeHTTPResponseFlags::ENDED | NodeHTTPResponseFlags::SOCKET_CLOSED) | ||
| }; | ||
| if is_ended_or_closed() { | ||
| return Ok(JSValue::FALSE); | ||
| } | ||
|
|
||
|
|
@@ -1761,6 +1759,10 @@ where | |
| fetch_headers_to_use | ||
| .fast_remove(HTTPHeaderName::SecWebSocketExtensions); | ||
| } | ||
| // Option getters and the headers conversion may have ended the response. | ||
| if is_ended_or_closed() { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: sed -n '1650,1795p' src/runtime/server/server_body.rs
rg -n -C 5 'fn upgrade|pub fn upgrade|NodeHTTPResponse::upgrade|\\.upgrade\\(' src/runtime/server
sed -n '1250,1345p' test/js/first_party/ws/ws.test.tsRepository: oven-sh/bun Length of output: 11158 🏁 Script executed: rg -n -C 8 'NodeHTTPResponse::upgrade|fn upgrade\(' src/runtime/server src/runtime | head -240
printf '\n--- relevant tests ---\n'
sed -n '1280,1395p' test/js/first_party/ws/ws.test.tsRepository: oven-sh/bun Length of output: 6924 🏁 Script executed: sed -n '527,610p' src/runtime/server/NodeHTTPResponse.rs
printf '\n--- all relevant upgrade regression tests ---\n'
rg -n -C 6 'options\.data|headers.*null|headers: null|headers: undefined|ends the response|bunServer\.upgrade\(res' test/js/first_party/ws/ws.test.tsRepository: oven-sh/bun Length of output: 5631 Recheck response state after all option getters.
Move the state check after the complete optional-options block and before 🤖 Prompt for AI Agents |
||
| return Ok(JSValue::FALSE); | ||
| } | ||
| if let Some(raw_response) = node_http_response.raw_response.get() { | ||
| // we must write the status first so that 200 OK isn't written | ||
| raw_response.write_status(b"101 Switching Protocols"); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟣 pre-existing, not blocking: pre-existing: an app that rejects the handshake with
socket.end(...)whileserver.upgrade(res, …)runs still getsupgrade()= true and a wsconnectionevent for the rejected client, as on base and in the linked issue's repro. The predicateis_ended_or_closedat src/runtime/server/server_body.rs:1676-1681 reads onlyNodeHTTPResponse.flags. The node:http socket'send()goes throughus_socket_buffered_js_write(raw write +shutdown()) and sets neither ENDED nor SOCKET_CLOSED. Fix: the predicate (used at both checks) must also refuse when the socket is shut down or the JSNodeHTTPServerSocket isended, e.g.us_socket_is_shut_downonraw_response.socket(). [also at: src/runtime/server/server_body.rs:1680 - pre-existing, partial fix: a ws app whosehandleProtocols(or any headers getter/toString) callssocket.end(...)still getsupgrade()returning true and a 'connection' callback for a socket it already ended — the linked issue's own repro.]A small fix can ride a push you are already making; otherwise a short reply is enough.
Extended reasoning...
Issue #43027's reproduction is a ws
handleProtocolsthat returns an object whosetoString()callssocket.end('HTTP/1.1 400 ...'). The ws shim (src/js/thirdparty/ws.js:1566-1569) passes that object asheaders: { 'sec-websocket-protocol': protocol }, sotoString()runs inside…Verification: pre-existing. Trigger: user code that runs inside
server.upgrade(res, { headers })(a header value'stoString(), a getter, an iterator — e.g. the linked issue's wshandleProtocolsobject) callssocket.end(...)on the node:http upgrade socket rather thanres.end(). The new guardis_ended_or_closed(src/runtime/server/server_body.rs:1676-1681) reads onlyNodeHTTPResponse.flagsfor…