Skip to content

Commit

Permalink
HTTP/2 Continuation test (dotnet#40533)
Browse files Browse the repository at this point in the history
  • Loading branch information
ManickaP authored Aug 11, 2020
1 parent c78acdc commit 302875d
Show file tree
Hide file tree
Showing 5 changed files with 138 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,8 @@ private async Task<Frame> ReadFrameAsync(CancellationToken cancellationToken)
return PingFrame.ReadFrom(header, data);
case FrameType.GoAway:
return GoAwayFrame.ReadFrom(header, data);
case FrameType.Continuation:
return ContinuationFrame.ReadFrom(header, data);
default:
return header;
}
Expand Down Expand Up @@ -531,26 +533,67 @@ public async Task<byte[]> ReadBodyAsync(bool expectEndOfStream = false)
return body;
}

public async Task<(int streamId, HttpRequestData requestData)> ReadAndParseRequestHeaderAsync(bool readBody = true)
public async IAsyncEnumerable<Frame> ReadRequestHeadersFrames()
{
HttpRequestData requestData = new HttpRequestData();

// Receive HEADERS frame for request.
Frame frame = await ReadFrameAsync(_timeout).ConfigureAwait(false);
if (frame == null)
{
throw new IOException("Failed to read Headers frame.");
}
Assert.Equal(FrameType.Headers, frame.Type);
HeadersFrame headersFrame = (HeadersFrame) frame;
Assert.Equal(FrameFlags.None, frame.Flags & ~(FrameFlags.EndStream | FrameFlags.EndHeaders));

yield return frame;

// Receive CONTINUATION frames for request.
while ((FrameFlags.EndHeaders & frame.Flags) != FrameFlags.EndHeaders)
{

frame = await ReadFrameAsync(_timeout).ConfigureAwait(false);
if (frame == null)
{
throw new IOException("Failed to read Continuation frame.");
}
Assert.Equal(FrameType.Continuation, frame.Type);
Assert.Equal(FrameFlags.None, frame.Flags & ~FrameFlags.EndHeaders);

yield return frame;
}

Assert.Equal(FrameFlags.EndHeaders, FrameFlags.EndHeaders & frame.Flags);
}

// TODO CONTINUATION support
Assert.Equal(FrameFlags.EndHeaders, FrameFlags.EndHeaders & headersFrame.Flags);
public async Task<(int streamId, HttpRequestData requestData)> ReadAndParseRequestHeaderAsync(bool readBody = true)
{
HttpRequestData requestData = new HttpRequestData();

int streamId = headersFrame.StreamId;
requestData.RequestId = streamId;
bool endOfStream = false;

using MemoryStream buffer = new MemoryStream();
await foreach (Frame frame in ReadRequestHeadersFrames())
{
switch (frame.Type)
{
case FrameType.Headers:
var headersFrame = (HeadersFrame)frame;
requestData.RequestId = headersFrame.StreamId;
endOfStream = (headersFrame.Flags & FrameFlags.EndStream) == FrameFlags.EndStream;
await buffer.WriteAsync(headersFrame.Data);
break;
case FrameType.Continuation:
var continuationFrame = (ContinuationFrame)frame;
await buffer.WriteAsync(continuationFrame.Data);
break;
default:
// Assert.Fail is already merged in xUnit but not released yet. Replace once available.
// https://github.com/xunit/xunit/issues/2105
Assert.True(false, $"Unexpected frame type '{frame.Type}'");
break;
}
}

Memory<byte> data = headersFrame.Data;
Memory<byte> data = buffer.ToArray();
int i = 0;
while (i < data.Length)
{
Expand All @@ -567,13 +610,13 @@ public async Task<byte[]> ReadBodyAsync(bool expectEndOfStream = false)
requestData.Method = requestData.GetSingleHeaderValue(":method");
requestData.Path = requestData.GetSingleHeaderValue(":path");

if (readBody && (frame.Flags & FrameFlags.EndStream) == 0)
if (readBody && !endOfStream)
{
// Read body until end of stream if needed.
requestData.Body = await ReadBodyAsync().ConfigureAwait(false);
}

return (streamId, requestData);
return (requestData.RequestId, requestData);
}

public override Task InitializeConnectionAsync()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,7 @@ private async Task<byte[]> ReadLineBytesAsync()
_readStart = 0;
_readEnd = dataLength;
_readBuffer = newBuffer;
startSearch = dataLength;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,15 @@ public static (int bytesConsumed, int value) DecodeInteger(ReadOnlySpan<byte> he

ulong extra = 0;
int length = 1;
byte b;
ulong b;

do
{
b = headerBlock[length++];
extra = checked(extra << 7) | b;
// https://http2.github.io/http2-spec/compression.html#integer.representation
// HPack encodes integers from the least significant byte to the most.
// Every 7-bits of the next byte is shifted by (7 * index) and added to the result.
b = (ulong)headerBlock[length++];
extra = checked(b << (7 * (length - 2))) | extra;
}
while ((b & 0b10000000) != 0);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,35 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async uri =>
});
}

[Fact]
public async Task SendAsync_LargeHeaders_CorrectlyWritten()
{
// Intentionally larger than 16K in total because that's the limit that will trigger a CONTINUATION frame in HTTP2.
string largeHeaderValue = new string('a', 1024);
int count = 20;

await LoopbackServerFactory.CreateClientAndServerAsync(async uri =>
{
using HttpClient client = CreateHttpClient();

var message = new HttpRequestMessage(HttpMethod.Get, uri) { Version = UseVersion };
for (int i = 0; i < count; i++)
{
message.Headers.TryAddWithoutValidation("large-header" + i, largeHeaderValue);
}
var response = await client.SendAsync(TestAsync, message).ConfigureAwait(false);
},
async server =>
{
HttpRequestData requestData = await server.HandleRequestAsync(HttpStatusCode.OK);

for (int i = 0; i < count; i++)
{
Assert.Equal(largeHeaderValue, requestData.GetSingleHeaderValue("large-header" + i));
}
});
}

[Fact]
public async Task SendAsync_DefaultHeaders_CorrectlyWritten()
{
Expand Down Expand Up @@ -383,7 +412,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync(

using HttpClient client = CreateHttpClient(handler);

await client.SendAsync(requestMessage);
await client.SendAsync(TestAsync, requestMessage);

foreach ((string name, _, _) in s_nonAsciiHeaders)
{
Expand Down Expand Up @@ -439,7 +468,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync(

using HttpClient client = CreateHttpClient(handler);

using HttpResponseMessage response = await client.SendAsync(requestMessage);
using HttpResponseMessage response = await client.SendAsync(TestAsync, requestMessage);

foreach ((string name, Encoding valueEncoding, string[] values) in s_nonAsciiHeaders)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2954,6 +2954,52 @@ public async Task Http2GetAsync_TrailigPseudo_Throw()
}
}

[Theory]
[InlineData(0)]
[InlineData(1)]
[InlineData(2)]
public async Task Http2SendAsync_LargeHeaders_CorrectlyWritten(int continuationCount)
{
// Intentionally larger than 2x16K in total because that's the limit that will trigger a CONTINUATION frame in HTTP2.
string largeHeaderValue = new string('a', 1024);
int count = continuationCount * 16 + 1;

await Http2LoopbackServer.CreateClientAndServerAsync(async uri =>
{
using HttpClient client = CreateHttpClient();

var message = new HttpRequestMessage(HttpMethod.Get, uri) { Version = UseVersion };
for (int i = 0; i < count; i++)
{
message.Headers.TryAddWithoutValidation("large-header" + i, largeHeaderValue);
}
var response = await client.SendAsync(TestAsync, message).ConfigureAwait(false);
},
async server =>
{
// Primes the connection, i.e. forces it to exchange SETTINGS whose ACK sometimes flushed the headers as well.
// See: https://github.com/dotnet/runtime/issues/860 for repro description.
(Http2LoopbackConnection connection, SettingsFrame clientSettings) = await server.EstablishConnectionGetSettingsAsync();

// Read individual frames and assert number of CONTINUATIONs.
int receivedContinuations = 0;
int streamId = 0;
await foreach (Frame frame in connection.ReadRequestHeadersFrames())
{
if (frame.Type == FrameType.Headers)
{
streamId = ((HeadersFrame)frame).StreamId;
}
if (frame.Type == FrameType.Continuation)
{
++receivedContinuations;
}
}
Assert.Equal(continuationCount, receivedContinuations);
await connection.SendResponseAsync(HttpStatusCode.OK, requestId: streamId);
});
}

[Fact]
public async Task InboundWindowSize_Exceeded_Throw()
{
Expand Down

0 comments on commit 302875d

Please sign in to comment.