Repository navigation
Conversation
…sed with H3_MESSAGE_ERROR instead of being served
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 #350.
What was wrong. RFC 9114 4.1.2 makes a request malformed when its content-length differs from the sum of its DATA payloads. A malformed request is a stream error of type H3_MESSAGE_ERROR. ioxide.http3 never read content-length: it was decoded into
req.Headersand never compared with anything.Bodyfor a request that announced a hundred.A proxy that forwards the header upstream would send a body shorter than the one it announces.
What changed.
Http3Request.AddFieldparses content-length as the field is decoded, into one internal field,ContentLengthLeft. It is -1 when there is no content-length and -2 when the value is invalid or repeated.long.TryParsewithNumberStyles.Noneaccepts digits only: no sign, no whitespace, no overflow.Http3Connectionchecks the value in three places:RefuseMalformed). It is a stream error, not a connection error:ParseState.Refused, which drops anything still in flight for it. Dropped bytes are still credited when the stream is paced.IOExceptionthat names the mismatch. This isHttp3BodyReader.Failplus oneException?field. The reader's docs now say so. Every other end (a fin, a reset, a teardown) is still an empty chunk, so the reader's documented contract is unchanged outside this one case.HeadersPayloadnow setsState = FrameHeaderbefore callingOnHeadersCompleteinstead of after, so a refusal there is not overwritten.Proof. All in
H3BodyTruncationTests. Its summary and the mid-frame test's rationale were updated where they had become false.http3: a body shorter than its content-length is refused, not served whole: the Pending, promoted (buffered, overH3TestClient).http3: a streamed body shorter than its content-length raises on read instead of ending: new.http3: a body that runs past its content-length is refused with H3_MESSAGE_ERROR: new.http3: an invalid or repeated content-length is refused with H3_MESSAGE_ERROR: new. Cases:ten,-10,+10,10and10+10, each with a ten-byte body.control: a body that matches its content-length is served whole(existing).control: a streamed body that matches its content-length reads as a clean end(new).control: a body that ends where its DATA frame said reads as a clean end(existing).RawH3Clientcan now write content-length fields and reports the code its request stream was reset with (WaitForReset). The wire tests assert 0x10e itself, becauseH3TestClientdoes not surface resets.Run both ways:
... the server answered 200 'got 10' and handed the handler a 10-byte body as if that were the whole requestexpected [raised IOException after 10], got [ended after 10]expected [reset 0x10e], got [served]content-length 'ten': expected reset 0x10e, got servedOne crash came up while writing this, and it was the test's fault, not the fix's. The first version of the invalid/repeated test segfaulted the E2E process inside
libioxide_ngtcp2.so: two reactor threads faulted on the same instruction. Its helper disposed eachQuicEnginewhile that engine's server still had live connections, andQuicEngine.Disposesays the engine must outlive them. The helper now stops the servers before disposing the engine, the wayQuicIdentityCapTestsdoes. There was no crash in the two full runs since.nghttp3 refuses all four malformed cases too, but as a connection error.
read_streamreturnsERR_MALFORMED_HTTP_MESSAGINGfor a short or long body, andERR_MALFORMED_HTTP_HEADERfor an invalid or repeated content-length.FailProtocolthen closes the whole connection with H3_MESSAGE_ERROR.H3TestClient: an exact body is served, and the other four came back with status -1 and the connection closed by the server.RFC 9114 section 8 lets an endpoint treat a stream error as a connection error, so nghttp3's behaviour is allowed. The two stacks now differ, though:
Suites.
ioxide.http3builds for net10.0 and net11.0 with no warnings.Bench.
bench/any.sh Http3/ManagedBuffered(h2load built with HTTP/3, 2 reactors pinned to CPUs 2 and 6, the driver to 0-1 and 12-15). Five rounds alternate main 71aab91, the whole batch merged (for h3, this PR, #382, #380 and the rebuilt QUIC .so), and main again as the A/A control:Flat.
Overlap. Merges cleanly with #382 (issue #351). Both touch
Http3Connection.cs, in different hunks. Checked withgit merge-treeagainst every other PR of this batch (#378, #379, #381, #376, #377, #380): all merge cleanly. #350 + #351 + #352 merged together build, and E2E passes there: 305 passed, 0 failed, 2 pending (the #195 and #349 tests).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:
using var enginein the test body, so the engine is disposed before the runner'sTestServer.StopAll(). That leaves a short window in which a reactor can run native code on a freed engine.QuicIdentityCapTestscloses the window with try/finally; the rest of the suite does not.Content-Lengthis never matched as content-length. nghttp3 refuses such a request.