Repository navigation
Conversation
This was referenced Oct 17, 2026
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.
Fixes #352.
What was wrong. A
QuicEnginewith no ALPN allow list confirms whatever the client offers, including nothing.Http3Connectionbackstops that: once its uni streams can open, it refuses anything buth3withH3_GENERAL_PROTOCOL_ERROR.Nghttp3Connectionnever readNegotiatedProtocol, so a connection that negotiatedechowas set up and answered 200, on all four run loops.What changed. The check now sits in
TrySetup, the first-wake setup that all four run loops share (buffered, buffered async, streaming and streamed response). It runs once the uni streams are open, the same point at which the pure-C# stack checks. Anything buth3is refused before nghttp3 is stood up, the same way the pure-C# stack refuses it:[ioxide.nghttp3] protocol error: negotiated 'echo', not h3);H3_GENERAL_PROTOCOL_ERROR(0x101).The code goes through the existing
_peerCode, and the caller'sFailInternalkeeps it, because the first failure wins. No state is added.One deliberate difference from
Http3Connection: the check is skipped once the connection is closed. A connection evicted before its handshake finished (by the idle sweep or a reactor stop) still wakes its handler. That wake arrives closed, withNegotiatedProtocolstill null and the uni streams still openable, becauseQuicRemoveConnectionwakes the handler inline before the engine is destroyed. Refusing it would log a protocol error and callCloseon every abandoned handshake, where the transport means an eviction to be silent. The closed wake keeps today's behavior.Proof. Tests:
H3AlpnTests:h3/nghttp3: a connection that did not negotiate h3 is not served, promoted from Pending.H3AlpnTests, new control:h3/nghttp3: control - a permissive engine serves a connection that negotiated h3. It uses the same engine and handler withh3offered, and gets a 200.H3ErrorCodeTests, new and at the code seam next to its pure-C# twin:h3/codes: nghttp3 - a non-h3 ALPN closes with H3_GENERAL_PROTOCOL_ERROR on every run loop, unless already closed.echoconnection with a GET queued. It must serve nothing and close with 0x101.:authority. nghttp3 refusesWellFormedGet's empty one asH3_MESSAGE_ERROR, which would make "not served" pass for the wrong reason.Run both ways:
an h3 handler must not serve a non-h3 connection, got 200 'served-by-nghttp3'.buffered: served 1 request(s) on a connection that negotiated 'echo'. A throwaway probe on the reverted source showed all four loops serve the GET (served=1, no close).an evicted connection was closed with 0x101.Suites (main 71aab91 vs. this branch):
ioxide.nghttp3builds for net10.0 and net11.0 with no warnings.Overlap. #382 (issue #351) also changes
H3ErrorCodeTests.cs.git merge-treeis clean against it and against this round's other PRs (quic: a second ClientHello naming a different host than the first is refused instead of answered #376, quic: a client common name carrying a control character is refused, as TCP refuses it #377, quic: a reactor shutdown tells its peers with a CONNECTION_CLOSE instead of leaving them to time out #380, http2: DATA on an idle stream is a PROTOCOL_ERROR instead of being credited and dropped #378, http2: a buffered response stays inside the peer's stream window and waits for credit instead of being reset #379, http3: a request whose body does not match its content-length is refused with H3_MESSAGE_ERROR instead of being served #383).H3ErrorCodeTests,H3CriticalStreamTests,H3AlpnTestsandHttp3Testsall pass on it (33 passed, 0 failed).All eight together. This batch's PRs (#376 to #383) were merged onto main 71aab91 in one scratch tree. The only conflict is the binary
libioxide_ngtcp2.sobetween #376 and #377, rebuilt there from the merged shim with the script's defaults. Everything builds, and Unit 102, E2E 309, Http 88, Chaos 80, Tls 169 (14 kTLS cases skipped) and File 5 pass, with 0 failed and 0 pending. The repo's only pending test left is the Redis suite's (#336).Noticed, not changed.
Http3Connection's backstop has no closed-connection skip. I swappedQuicSniHostileTests' handler forHttp3Connectionin a throwaway run: every half-open connection's eviction logs[ioxide.http3] protocol error: negotiated '(none)', not h3and callsClose(0x101). The same one-condition skip (&& !IsClosed) would quiet it. That code is in ioxide.http3, so it is not in this PR.TrySetup's comment "Runs on the first post-handshake wake" is not true for those evictions.