Skip to content

Commit

Permalink
HTTP/3: Use all static status values (#38605)
Browse files Browse the repository at this point in the history
  • Loading branch information
JamesNK authored Nov 26, 2021
1 parent 8817967 commit d9660d1
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 87 deletions.
23 changes: 9 additions & 14 deletions src/Servers/Kestrel/Core/src/Internal/Http2/HPackHeaderWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,21 +73,16 @@ public static bool ContinueEncodeHeaders(DynamicHPackEncoder hpackEncoder, Http2

private static bool EncodeStatusHeader(int statusCode, DynamicHPackEncoder hpackEncoder, Span<byte> buffer, out int length)
{
switch (statusCode)
if (H2StaticTable.TryGetStatusIndex(statusCode, out var index))
{
case 200:
case 204:
case 206:
case 304:
case 400:
case 404:
case 500:
// Status codes which exist in the HTTP/2 StaticTable.
return HPackEncoder.EncodeIndexedHeaderField(H2StaticTable.GetStatusIndex(statusCode), buffer, out length);
default:
const string name = ":status";
var value = StatusCodes.ToStatusString(statusCode);
return hpackEncoder.EncodeHeader(buffer, H2StaticTable.Status200, HeaderEncodingHint.Index, name, value, valueEncoding: null, out length);
// Status codes which exist in the HTTP/2 StaticTable.
return HPackEncoder.EncodeIndexedHeaderField(index, buffer, out length);
}
else
{
const string name = ":status";
var value = StatusCodes.ToStatusString(statusCode);
return hpackEncoder.EncodeHeader(buffer, H2StaticTable.Status200, HeaderEncodingHint.Index, name, value, valueEncoding: null, out length);
}
}

Expand Down
37 changes: 16 additions & 21 deletions src/Servers/Kestrel/Core/src/Internal/Http3/QPackHeaderWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,28 +91,23 @@ private static bool EncodeHeader(Span<byte> buffer, int staticTableId, string na

private static int EncodeStatusCode(int statusCode, Span<byte> buffer)
{
switch (statusCode)
if (H3StaticTable.TryGetStatusIndex(statusCode, out var index))
{
case 200:
case 204:
case 206:
case 304:
case 400:
case 404:
case 500:
QPackEncoder.EncodeStaticIndexedHeaderField(H3StaticTable.StatusIndex[statusCode], buffer, out var bytesWritten);
return bytesWritten;
default:
// https://tools.ietf.org/html/draft-ietf-quic-qpack-21#section-4.5.4
// Index is 63 - :status
buffer[0] = 0b01011111;
buffer[1] = 0b00110000;

ReadOnlySpan<byte> statusBytes = System.Net.Http.HPack.StatusCodes.ToStatusBytes(statusCode);
buffer[2] = (byte)statusBytes.Length;
statusBytes.CopyTo(buffer.Slice(3));

return 3 + statusBytes.Length;
QPackEncoder.EncodeStaticIndexedHeaderField(index, buffer, out var bytesWritten);
return bytesWritten;
}
else
{
// https://tools.ietf.org/html/draft-ietf-quic-qpack-21#section-4.5.4
// Index is 63 - :status
buffer[0] = 0b01011111;
buffer[1] = 0b00110000;

ReadOnlySpan<byte> statusBytes = System.Net.Http.HPack.StatusCodes.ToStatusBytes(statusCode);
buffer[2] = (byte)statusBytes.Length;
statusBytes.CopyTo(buffer.Slice(3));

return 3 + statusBytes.Length;
}
}
}
38 changes: 34 additions & 4 deletions src/Servers/Kestrel/Core/test/Http3/Http3QPackEncoderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ public void BeginEncodeHeaders_StatusWithoutIndexedValue_WriteIndexNameAndFullVa
var enumerator = new Http3HeadersEnumerator();
enumerator.Initialize(headers);

Assert.True(QPackHeaderWriter.BeginEncodeHeaders(302, enumerator, buffer, ref totalHeaderSize, out var length));
Assert.True(QPackHeaderWriter.BeginEncodeHeaders(418, enumerator, buffer, ref totalHeaderSize, out var length));

var result = buffer.Slice(0, length).ToArray();
var hex = BitConverter.ToString(result);
Assert.Equal("00-00-5F-30-03-33-30-32", hex);
Assert.Equal("00-00-5F-30-03-34-31-38", hex);
}

[Fact]
Expand All @@ -43,6 +43,36 @@ public void BeginEncodeHeaders_StatusWithIndexedValue_WriteIndex()
Assert.Equal("00-00-D9", hex);
}

[Theory]
[InlineData(103)]
[InlineData(200)]
[InlineData(304)]
[InlineData(404)]
[InlineData(503)]
[InlineData(100)]
[InlineData(204)]
[InlineData(206)]
[InlineData(302)]
[InlineData(400)]
[InlineData(403)]
[InlineData(421)]
[InlineData(425)]
[InlineData(500)]
public void BeginEncodeHeaders_StatusWithIndexedValue_ExpectedLength(int statusCode)
{
Span<byte> buffer = new byte[1024 * 16];

var totalHeaderSize = 0;
var headers = new HttpResponseHeaders();
var enumerator = new Http3HeadersEnumerator();
enumerator.Initialize(headers);

Assert.True(QPackHeaderWriter.BeginEncodeHeaders(statusCode, enumerator, buffer, ref totalHeaderSize, out var length));
length -= 2; // Remove prefix

Assert.True(length <= 2, "Indexed header should be encoded into 1 or 2 bytes");
}

[Fact]
public void BeginEncodeHeaders_NonStaticKey_WriteFullNameAndFullValue()
{
Expand All @@ -55,9 +85,9 @@ public void BeginEncodeHeaders_NonStaticKey_WriteFullNameAndFullValue()
var enumerator = new Http3HeadersEnumerator();
enumerator.Initialize(headers);

Assert.True(QPackHeaderWriter.BeginEncodeHeaders(302, enumerator, buffer, ref totalHeaderSize, out var length));
Assert.True(QPackHeaderWriter.BeginEncodeHeaders(enumerator, buffer, ref totalHeaderSize, out var length));

var result = buffer.Slice(8, length - 8).ToArray();
var result = buffer.Slice(2, length - 2).ToArray(); // trim prefix
var hex = BitConverter.ToString(result);
Assert.Equal("37-02-74-72-61-6E-73-6C-61-74-65-07-70-72-69-76-61-74-65", hex);
}
Expand Down
10 changes: 7 additions & 3 deletions src/Shared/runtime/Http2/Hpack/H2StaticTable.Http2.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ internal static partial class H2StaticTable

public static ref readonly HeaderField Get(int index) => ref s_staticDecoderTable[index];

public static int GetStatusIndex(int status) =>
status switch
public static bool TryGetStatusIndex(int status, out int index)
{
index = status switch
{
200 => 8,
204 => 9,
Expand All @@ -21,9 +22,12 @@ public static int GetStatusIndex(int status) =>
400 => 12,
404 => 13,
500 => 14,
_ => throw new ArgumentOutOfRangeException(nameof(status))
_ => -1
};

return index != -1;
}

private static readonly HeaderField[] s_staticDecoderTable = new HeaderField[]
{
CreateHeaderField(":authority", ""),
Expand Down
47 changes: 21 additions & 26 deletions src/Shared/runtime/Http2/Hpack/HPackEncoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,36 +44,31 @@ public static bool EncodeIndexedHeaderField(int index, Span<byte> destination, o
public static bool EncodeStatusHeader(int statusCode, Span<byte> destination, out int bytesWritten)
{
// Bytes written depend on whether the status code value maps directly to an index
switch (statusCode)
if (H2StaticTable.TryGetStatusIndex(statusCode, out var index))
{
case 200:
case 204:
case 206:
case 304:
case 400:
case 404:
case 500:
// Status codes which exist in the HTTP/2 StaticTable.
return EncodeIndexedHeaderField(H2StaticTable.GetStatusIndex(statusCode), destination, out bytesWritten);
default:
// If the status code doesn't have a static index then we need to include the full value.
// Write a status index and then the number bytes as a string literal.
if (!EncodeLiteralHeaderFieldWithoutIndexing(H2StaticTable.Status200, destination, out var nameLength))
{
bytesWritten = 0;
return false;
}
// Status codes which exist in the HTTP/2 StaticTable.
return EncodeIndexedHeaderField(index, destination, out bytesWritten);
}
else
{
// If the status code doesn't have a static index then we need to include the full value.
// Write a status index and then the number bytes as a string literal.
if (!EncodeLiteralHeaderFieldWithoutIndexing(H2StaticTable.Status200, destination, out var nameLength))
{
bytesWritten = 0;
return false;
}

var statusBytes = StatusCodes.ToStatusBytes(statusCode);
var statusBytes = StatusCodes.ToStatusBytes(statusCode);

if (!EncodeStringLiteral(statusBytes, destination.Slice(nameLength), out var valueLength))
{
bytesWritten = 0;
return false;
}
if (!EncodeStringLiteral(statusBytes, destination.Slice(nameLength), out var valueLength))
{
bytesWritten = 0;
return false;
}

bytesWritten = nameLength + valueLength;
return true;
bytesWritten = nameLength + valueLength;
return true;
}
}

Expand Down
43 changes: 24 additions & 19 deletions src/Shared/runtime/Http3/QPack/H3StaticTable.Http3.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,6 @@ namespace System.Net.Http.QPack
{
internal static partial class H3StaticTable
{
private static readonly Dictionary<int, int> s_statusIndex = new Dictionary<int, int>
{
[103] = 24,
[200] = 25,
[304] = 26,
[404] = 27,
[503] = 28,
[100] = 63,
[204] = 64,
[206] = 65,
[302] = 66,
[400] = 67,
[403] = 68,
[421] = 69,
[425] = 70,
[500] = 71,
};

private static readonly Dictionary<HttpMethod, int> s_methodIndex = new Dictionary<HttpMethod, int>
{
// TODO connect is internal to system.net.http
Expand All @@ -37,10 +19,33 @@ internal static partial class H3StaticTable
[HttpMethod.Put] = 21,
};

public static bool TryGetStatusIndex(int status, out int index)
{
index = status switch
{
103 => 24,
200 => 25,
304 => 26,
404 => 27,
503 => 28,
100 => 63,
204 => 64,
206 => 65,
302 => 66,
400 => 67,
403 => 68,
421 => 69,
425 => 70,
500 => 71,
_ => -1
};

return index != -1;
}

public static int Count => s_staticTable.Length;

// TODO: just use Dictionary directly to avoid interface dispatch.
public static IReadOnlyDictionary<int, int> StatusIndex => s_statusIndex;
public static IReadOnlyDictionary<HttpMethod, int> MethodIndex => s_methodIndex;

public static HeaderField GetHeaderFieldAt(int index) => s_staticTable[index];
Expand Down

0 comments on commit d9660d1

Please sign in to comment.