feat(upgrade): allow resetting an HTTP/2 upgrade with CONNECT_ERROR - #4210
Open
jeremyjpj0916 wants to merge 2 commits into
Open
jeremyjpj0916 wants to merge 2 commits into
jeremyjpj0916 wants to merge 2 commits into
Conversation
Dropping or shutting down an `Upgraded` that came from an HTTP/2 `CONNECT` always ends the stream with `END_STREAM`, so a proxy whose tunnelled TCP connection fails cannot report it as RFC 9113 section 8.5 asks. Add `Upgraded::reset_with_connect_error`. For an HTTP/2 upgrade it queues `RST_STREAM(CONNECT_ERROR)` in place of the `END_STREAM` and returns `true`. It returns `false` for any other upgrade, for a second call, or once the send side has finished. `H2Upgraded` sends the reason over a new oneshot to `UpgradedSendStreamTask`, which polls it right after `poll_reset`, ahead of buffered or queued data, so a reset followed at once by a drop still sends `RST_STREAM`. `Upgraded` reaches `H2Upgraded` through a `&mut` form of the existing `TypeId` downcast. Closes hyperium#4209
`UpgradedSendStreamTask` now clears its reset receiver as soon as it finishes, before it sends or drops its error. A writer that has seen the task end therefore always gets `false` from a later reset, whatever the executor does with the finished future. The field order stays as a fallback for a task dropped before it finishes. Add a test that resets while a buffered write waits for flow-control capacity, and a Miri-runnable test for the success path of `__hyper_downcast_mut`. Expand that function's SAFETY comment, and explain why `reset_with_connect_error` derefs through the `Box`.
jeremyjpj0916
added a commit
to ferrum-edge/ferrum-edge
that referenced
this pull request
Sep 28, 2026
The vendored hyper CONNECT_ERROR patch is filed as hyperium/hyper#4209 and hyperium/hyper#4210. Update the patch README, inventory row, lifecycle JSON, and filed texts, and note the one known difference from the upstream PR. Add the operator-facing note on what a revoked HBONE tunnel looks like and how to tell a revocation from a backend failure on the gateway.
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.
Closes #4209
Problem
Dropping or shutting down an
Upgradedthat came from an HTTP/2CONNECTalways ends the stream with
END_STREAM.H2Upgradedholds only the datachannel into
UpgradedSendStreamTask, which owns theSendStream, andH2Upgradedis private, so user code cannot reachSendStream::send_reset.A proxy whose tunnelled TCP connection fails therefore has no way to follow
RFC 9113 section 8.5, which treats any error in that TCP connection as a
stream error of type
CONNECT_ERROR.The client sees a clean close and cannot tell a failed tunnel from a complete
one.
Change
Add one public method:
proto::h2::upgrade::paircreates a second oneshot.H2Upgradedkeeps thesender (
H2Upgraded::reset).UpgradedSendStreamTaskpolls the receiverright after its
poll_resetcheck, before it waits for capacity for abuffered write or reads the data channel. On a reason it calls
send_reset(reason)and ends. On a dropped sender it clears the receiverand carries on as before.
still sends
RST_STREAM, notEND_STREAM, and a buffered write waiting forcapacity does not delay it.
error. So once a writer sees the task gone (for example a completed
poll_shutdown), a later reset returnsfalse, whatever the executor doeswith the finished future.
UpgradedreachesH2Upgradedwith a&mutform of the existingTypeIddowncast (modeled on
std::error::Error::downcast_mut) via a newRewind::get_mut.H2Upgradedbecomespub(crate).The method takes no error code, because
h2::Reasonis not part of hyper'spublic API and
CONNECT_ERRORis the code the RFC assigns to this case. Avariant taking a code is easy to add if preferred.
Cost: one more small allocation per HTTP/2 upgrade (the oneshot) and one
oneshot poll per wake of the send task. Nothing changes for a stream that is
never reset.
An earlier form of this change is carried on Ferrum Edge's main branch as a patch on a vendored
hyper 1.9.0, not yet released.
Tests
In
src/proto/h2/upgrade.rs, each test runs a real h2 client and server overtokio::io::duplex, with the server side wrapped inUpgraded:RST_STREAM(CONNECT_ERROR); a second reset returnsfalse;grants a zero stream window) reaches the client as
RST_STREAM(CONNECT_ERROR), with no DATA;END_STREAM;poll_shutdown, the reset returnsfalseand the clientsees
END_STREAM.In
src/upgrade.rs, which also run under Miri:falseand still downcasts;&mutdowncast returnsNonefor the wrong type, and a change madethrough it to the IO inside
Upgradedis visible afterwards.