diff --git a/src/protocols/ioxide.http2/Http2Connection.Frames.cs b/src/protocols/ioxide.http2/Http2Connection.Frames.cs index 222c4586..14be8c85 100644 --- a/src/protocols/ioxide.http2/Http2Connection.Frames.cs +++ b/src/protocols/ioxide.http2/Http2Connection.Frames.cs @@ -193,6 +193,7 @@ private void HandleHeaders(in FrameHeader header, ReadOnlySpan payload) // A header block can span HEADERS + CONTINUATION frames, and HPACK cannot be decoded // piecewise - the whole block has to be in hand first. + pending.HeadersDone = false; // trailers open a block of their own, and the request waits for its END_HEADERS pending.AppendHeaderBlock(block); if (pending.HeaderBlock.Length > _options.MaxHeaderListSize) diff --git a/tests/Ioxide.Tests.Unit/Http2TrailerTests.cs b/tests/Ioxide.Tests.Unit/Http2TrailerTests.cs index 1e1db4d5..1cb6d942 100644 --- a/tests/Ioxide.Tests.Unit/Http2TrailerTests.cs +++ b/tests/Ioxide.Tests.Unit/Http2TrailerTests.cs @@ -148,6 +148,33 @@ public static void Register(Runner runner) peer.Close(run); }); + runner.Test("h2 request: a buffered request's trailers split over HEADERS and CONTINUATION merge into it, and the connection serves on", () => + { + var served = new List(); + + using var peer = new Peer(new Http2Options()); + Task run = peer.Connection.RunAsync(request => + { + request.TryGetHeader("x-checksum"u8, out ReadOnlyMemory checksum); + request.TryGetHeader("x-digest"u8, out ReadOnlyMemory digest); + served.Add($"{request.StreamId} '{Ascii(request.Path)}' {request.Body.Length} x-checksum '{Ascii(checksum)}' x-digest '{Ascii(digest)}'"); + return Http2Response.Text("done"); + }); + + peer.Open(); + peer.Headers(1, Request("/upload"), endStream: false); + peer.Data(1, 400, endStream: false); + peer.HeadersOpen(1, Field("x-checksum", "abc"), endStream: true); // END_STREAM, with the block still open + peer.Continuation(1, Field("x-digest", "def")); // END_HEADERS + peer.Headers(3, Request("/next"), endStream: true); + + Assert.True(served.SequenceEqual(["1 '/upload' 400 x-checksum 'abc' x-digest 'def'", "3 '/next' 0 x-checksum '' x-digest ''"]), + $"the handler was handed: {string.Join(", ", served)}"); + Assert.Equal("", GoAways(peer)); + + peer.Close(run); + }); + runner.Test("h2 request: a stream refused past MaxConcurrentStreams still gets REFUSED_STREAM, and its id opens nothing later", () => { var served = new List(); @@ -343,6 +370,10 @@ public void Headers(int streamId, byte[] block, bool endStream) public void Continuation(int streamId, byte[] block) => Feed(FrameBytes(0x9, 0x4, streamId, block)); + /// A HEADERS frame without END_HEADERS: a CONTINUATION ends its block. + public void HeadersOpen(int streamId, byte[] block, bool endStream) + => Feed(FrameBytes(0x1, (byte)(endStream ? 0x1 : 0), streamId, block)); + public void Data(int streamId, int bytes, bool endStream) => Feed(FrameBytes(0x0, (byte)(endStream ? 0x1 : 0), streamId, Enumerable.Repeat((byte)'z', bytes).ToArray()));