Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/protocols/ioxide.http2/Http2Connection.Frames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,14 @@ private void HandleHeaders(in FrameHeader header, ReadOnlySpan<byte> payload)
}
return;
}
else if ((header.Flags & FrameFlags.EndStream) == 0)
{
// RFC 9113 8.1: after the block that opens a request come only trailers, which end its stream.
_discardBlock.StreamId = header.StreamId;
DiscardHeaderBlock(header, block);
ResetStream(header.StreamId, Http2Error.ProtocolError);
return;
}

// A header block can span HEADERS + CONTINUATION frames, and HPACK cannot be decoded
// piecewise - the whole block has to be in hand first.
Expand Down
54 changes: 54 additions & 0 deletions tests/Ioxide.Tests.Unit/Http2TrailerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,56 @@ public static void Register(Runner runner)
gate.SetResult();
peer.Close(run);
});

// RFC 9113 8.1. Its fields landed in the arena the body was being appended to, and the body's range ran across them.
runner.Test("h2 buffered request: HEADERS without END_STREAM after the opening block resets the stream, and its body never reaches a handler", () =>
{
var served = new List<string>();

using var peer = new Peer(new Http2Options());
Task run = peer.Connection.RunAsync(request =>
{
served.Add($"{Ascii(request.Path)} {Fields(request)}");
return Http2Response.Text("ok");
});

peer.Open();
peer.Headers(1, Request("/up"), endStream: false);
peer.Data(1, 400, endStream: false);
peer.Headers(1, Field("x-a", "abc", index: true), endStream: false);
peer.Data(1, 400, endStream: true);
peer.Headers(3, [.. Request("/next"), 0xBE], endStream: true); // the block was decoded, so x-a is at index 62
peer.Close(run);

Assert.Equal("1 PROTOCOL_ERROR", Resets(peer));
Assert.Equal("", GoAways(peer));
Assert.True(served.SequenceEqual(["/next x-a: abc"]), $"the handler was handed: {string.Join(", ", served)}");
});

// The block's CONTINUATION is still owed after the reset, and has to reach the decoder, not end the connection.
runner.Test("h2 buffered request: HEADERS without END_STREAM split over CONTINUATION still keeps HPACK in step", () =>
{
var served = new List<string>();

using var peer = new Peer(new Http2Options());
Task run = peer.Connection.RunAsync(request =>
{
served.Add($"{Ascii(request.Path)} {Fields(request)}");
return Http2Response.Text("ok");
});

peer.Open();
peer.Headers(1, Request("/up"), endStream: false);
byte[] block = Field("x-a", "abc", index: true);
peer.HeadersOpen(1, block[..3], endStream: false);
peer.Continuation(1, block[3..]);
peer.Headers(3, [.. Request("/next"), 0xBE], endStream: true);
peer.Close(run);

Assert.Equal("1 PROTOCOL_ERROR", Resets(peer));
Assert.Equal("", GoAways(peer));
Assert.True(served.SequenceEqual(["/next x-a: abc"]), $"the handler was handed: {string.Join(", ", served)}");
});
}

// :method POST and :scheme http from the static table, and :path as a literal on its static
Expand Down Expand Up @@ -343,6 +393,10 @@ public void Headers(int streamId, byte[] block, bool endStream)

public void Continuation(int streamId, byte[] block) => Feed(FrameBytes(0x9, 0x4, streamId, block));

/// <summary>A HEADERS frame without END_HEADERS: a CONTINUATION ends its block.</summary>
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()));

Expand Down
Loading