Repository navigation
A refused upload keeps its answer, no clear-text 502 in a tunnel, and OPTIONS * keeps its host - #702
Merged
dylan-conway merged 3 commits intoOct 14, 2026
Conversation
…, and OPTIONS * keeps its host A request the parser refuses in a tunnel had its socket destroyed as soon as the 400 was flushed. A body still on its way, unread, turned the close into a reset, which discarded the answer. The socket is ended, and destroyed a second later, as after a denial. The mux's handler for a failed dial ran on any error of the backend connection, so a backend that went away under an upload had a clear-text 502 written into an established connection, a TLS tunnel included. Past the dial the error is logged, and the close that follows ends the client. The URL given to filterRequest for the asterisk form was the authority and the "*" put together, which reads the "*" into the host. Its path is empty (RFC 9112 3.3).
The condition was that the dial had failed. But the mux is in front of plain HTTP too, and a backend that fails after the dial and before any reply left the client a bare close where it used to get a readable 502. What makes the clear text harmful is that something has already gone back.
Whether a backend that goes away shows at the mux as an error is the platform's. On macOS and Windows the client in this case is just closed, so there is no 502 to tell apart.
dylan-conway
merged commit Oct 14, 2026
9ce832f
into
fix/never-rewrite-a-request-target-silently
16 checks passed
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.
Stacked on #698 (base: its branch). Three small ends of the same work. Parts 1 and 3 correct code that is in no release.
1. A refused upload keeps its answer
#698 answers 400 to a request the HTTP parser refuses in a terminated tunnel, and destroyed the socket as soon as the answer was flushed. With a body still on its way, unread, the close became a reset, and the reset discarded the answer. It now ends the socket and destroys it a second later, as a denial by
filterRequestdoes.curl --data-binary @file 'https://host/x?q=café', direct and through twosocatrelays:curl: (56))The violation was recorded either way.
2. The mux's 502 is an answer, for a client that has had none
Its handler for a failed dial ran on any error of the backend connection, at any time. So a backend that went away under an upload had
HTTP/1.1 502 Bad Gateway … mux backend dial failed (EPIPE)written, in clear text, into whatever the client was in the middle of. In a TLS tunnel that is whatcurlreports aswrong version number. As in 0.0.79.A relay recorded what went back to
curlpast the CONNECT's own answer, over 30 refused uploads:The 502 is now written only where nothing has gone back to the client yet: a dial that fails, or a backend that fails before its first reply, on plain HTTP as in a CONNECT. There the client is waiting for an answer, and this is one. After that the error is logged, and the
closethat follows ends the client.Any bytes count as something gone back, an interim
100 Continueincluded. A backend that fails right after one leaves a bare close. To tell that apart the mux would have to parse status lines, which it does not.3.
OPTIONS *is judged under its own hostThe URL for
filterRequestwas the authority and the target put together, so the asterisk form gavehttps://host*. A callback that reads the host off the URL gothost*. (With a port in the authority the URL did not parse, and the request got 403.) Since #663; 0.0.79 closed the tunnel and never asked.RFC 9112 3.3: the target URI of the asterisk form has an empty path. So the callback gets
https://host/, andrequestTargetsays*, which is what goes on the wire.Testing
Each part has a test that fails without it, run on all four combinations of parts 1 and 2:
curlas a child process, throughSandboxManager. A client in the test's own event loop reads the answer the moment it is there and shows nothing; so doescurlwith no mux in front.37 network test files on Bun 1.4.2 and 1.4.3: nothing fails that does not on #698.