Skip to content

Commit e4f0d54

Browse files
author
ladeak
committed
Initial implementation
1 parent 7033ec7 commit e4f0d54

File tree

6 files changed

+386
-86
lines changed

6 files changed

+386
-86
lines changed

src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs

Lines changed: 54 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,8 @@ internal sealed class Http2FrameWriter
7171
// This is only set to true by tests.
7272
private readonly bool _scheduleInline;
7373

74-
private uint _maxFrameSize = Http2PeerSettings.MinAllowedMaxFrameSize;
75-
private byte[] _headerEncodingBuffer;
74+
private int _maxFrameSize = Http2PeerSettings.MinAllowedMaxFrameSize;
75+
private readonly ArrayBufferWriter<byte> _headerEncodingBuffer;
7676
private long _unflushedBytes;
7777

7878
private bool _completed;
@@ -107,7 +107,7 @@ public Http2FrameWriter(
107107
_flusher = new TimingPipeFlusher(timeoutControl, serviceContext.Log);
108108
_flusher.Initialize(_outputWriter);
109109
_outgoingFrame = new Http2Frame();
110-
_headerEncodingBuffer = new byte[_maxFrameSize];
110+
_headerEncodingBuffer = new ArrayBufferWriter<byte>(_maxFrameSize);
111111

112112
_scheduleInline = serviceContext.Scheduler == PipeScheduler.Inline;
113113

@@ -373,8 +373,9 @@ public void UpdateMaxFrameSize(uint maxFrameSize)
373373
{
374374
if (_maxFrameSize != maxFrameSize)
375375
{
376-
_maxFrameSize = maxFrameSize;
377-
_headerEncodingBuffer = new byte[_maxFrameSize];
376+
// Safe cast, MaxFrameSize is limited to 2^24-1 bytes by the protocol and by Http2PeerSettings.
377+
// Ref: https://datatracker.ietf.org/doc/html/rfc7540#section-4.2
378+
_maxFrameSize = (int)maxFrameSize;
378379
}
379380
}
380381
}
@@ -485,8 +486,11 @@ private void WriteResponseHeadersUnsynchronized(int streamId, int statusCode, Ht
485486
{
486487
_headersEnumerator.Initialize(headers);
487488
_outgoingFrame.PrepareHeaders(headerFrameFlags, streamId);
488-
var buffer = _headerEncodingBuffer.AsSpan();
489+
_headerEncodingBuffer.ResetWrittenCount();
490+
var buffer = _headerEncodingBuffer.GetSpan(_maxFrameSize)[0.._maxFrameSize];
489491
var done = HPackHeaderWriter.BeginEncodeHeaders(statusCode, _hpackEncoder, _headersEnumerator, buffer, out var payloadLength);
492+
Debug.Assert(done != HeaderWriteResult.BufferTooSmall, "Oversized frames should not be returned, beucase this always writes the status.");
493+
_headerEncodingBuffer.Advance(payloadLength);
490494
FinishWritingHeaders(streamId, payloadLength, done);
491495
}
492496
// Any exception from the HPack encoder can leave the dynamic table in a corrupt state.
@@ -524,10 +528,18 @@ private ValueTask<FlushResult> WriteDataAndTrailersAsync(Http2Stream stream, in
524528

525529
try
526530
{
527-
_headersEnumerator.Initialize(headers);
528531
_outgoingFrame.PrepareHeaders(Http2HeadersFrameFlags.END_STREAM, streamId);
529-
var buffer = _headerEncodingBuffer.AsSpan();
530-
var done = HPackHeaderWriter.BeginEncodeHeaders(_hpackEncoder, _headersEnumerator, buffer, out var payloadLength);
532+
HeaderWriteResult done = HeaderWriteResult.MoreHeaders;
533+
int payloadLength;
534+
do
535+
{
536+
_headersEnumerator.Initialize(headers);
537+
_headerEncodingBuffer.ResetWrittenCount();
538+
var bufferSize = done == HeaderWriteResult.BufferTooSmall ? _headerEncodingBuffer.Capacity * 2 : _headerEncodingBuffer.Capacity;
539+
var buffer = _headerEncodingBuffer.GetSpan(bufferSize)[0..bufferSize];
540+
done = HPackHeaderWriter.BeginEncodeHeaders(_hpackEncoder, _headersEnumerator, buffer, out payloadLength);
541+
} while (done == HeaderWriteResult.BufferTooSmall);
542+
_headerEncodingBuffer.Advance(payloadLength);
531543
FinishWritingHeaders(streamId, payloadLength, done);
532544
}
533545
// Any exception from the HPack encoder can leave the dynamic table in a corrupt state.
@@ -542,32 +554,45 @@ private ValueTask<FlushResult> WriteDataAndTrailersAsync(Http2Stream stream, in
542554
}
543555
}
544556

545-
private void FinishWritingHeaders(int streamId, int payloadLength, bool done)
557+
private void SplitHeaderFramesToOutput(int streamId, HeaderWriteResult done, bool isFramePrepared)
546558
{
547-
var buffer = _headerEncodingBuffer.AsSpan();
548-
_outgoingFrame.PayloadLength = payloadLength;
549-
if (done)
559+
var dataToFrame = _headerEncodingBuffer.WrittenSpan;
560+
var shouldPrepareFrame = !isFramePrepared;
561+
while (dataToFrame.Length > 0)
550562
{
551-
_outgoingFrame.HeadersFlags |= Http2HeadersFrameFlags.END_HEADERS;
552-
}
553-
554-
WriteHeaderUnsynchronized();
555-
_outputWriter.Write(buffer.Slice(0, payloadLength));
556-
557-
while (!done)
558-
{
559-
_outgoingFrame.PrepareContinuation(Http2ContinuationFrameFlags.NONE, streamId);
560-
561-
done = HPackHeaderWriter.ContinueEncodeHeaders(_hpackEncoder, _headersEnumerator, buffer, out payloadLength);
562-
_outgoingFrame.PayloadLength = payloadLength;
563+
if (shouldPrepareFrame)
564+
{
565+
_outgoingFrame.PrepareContinuation(Http2ContinuationFrameFlags.NONE, streamId);
566+
}
567+
else
568+
{
569+
shouldPrepareFrame = true;
570+
}
563571

564-
if (done)
572+
var currentSize = dataToFrame.Length > _maxFrameSize ? _maxFrameSize : dataToFrame.Length;
573+
_outgoingFrame.PayloadLength = currentSize;
574+
if (done == HeaderWriteResult.Done && dataToFrame.Length == currentSize)
565575
{
566-
_outgoingFrame.ContinuationFlags = Http2ContinuationFrameFlags.END_HEADERS;
576+
_outgoingFrame.HeadersFlags |= Http2HeadersFrameFlags.END_HEADERS;
567577
}
568578

569579
WriteHeaderUnsynchronized();
570-
_outputWriter.Write(buffer.Slice(0, payloadLength));
580+
_outputWriter.Write(dataToFrame[..currentSize]);
581+
dataToFrame = dataToFrame.Slice(currentSize);
582+
}
583+
}
584+
585+
private void FinishWritingHeaders(int streamId, int payloadLength, HeaderWriteResult done)
586+
{
587+
SplitHeaderFramesToOutput(streamId, done, isFramePrepared: true);
588+
while (done != HeaderWriteResult.Done)
589+
{
590+
_headerEncodingBuffer.ResetWrittenCount();
591+
var bufferSize = done == HeaderWriteResult.BufferTooSmall ? _headerEncodingBuffer.Capacity * 2 : _headerEncodingBuffer.Capacity;
592+
var buffer = _headerEncodingBuffer.GetSpan(bufferSize)[0..bufferSize];
593+
done = HPackHeaderWriter.ContinueEncodeHeaders(_hpackEncoder, _headersEnumerator, buffer, out payloadLength);
594+
_headerEncodingBuffer.Advance(payloadLength);
595+
SplitHeaderFramesToOutput(streamId, done, isFramePrepared: false);
571596
}
572597
}
573598

@@ -994,4 +1019,4 @@ private void EnqueueWaitingForMoreConnectionWindow(Http2OutputProducer producer)
9941019
_http2Connection.Abort(new ConnectionAbortedException("HTTP/2 connection exceeded the outgoing flow control maximum queue size."));
9951020
}
9961021
}
997-
}
1022+
}

src/Servers/Kestrel/Core/test/Http2/Http2HPackEncoderTests.cs

Lines changed: 75 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public void BeginEncodeHeaders_Status302_NewIndexValue()
2929
enumerator.Initialize(headers);
3030

3131
var hpackEncoder = new DynamicHPackEncoder();
32-
Assert.True(HPackHeaderWriter.BeginEncodeHeaders(302, hpackEncoder, enumerator, buffer, out var length));
32+
Assert.Equal(HeaderWriteResult.Done, HPackHeaderWriter.BeginEncodeHeaders(302, hpackEncoder, enumerator, buffer, out var length));
3333

3434
var result = buffer.Slice(0, length).ToArray();
3535
var hex = BitConverter.ToString(result);
@@ -52,7 +52,7 @@ public void BeginEncodeHeaders_CacheControlPrivate_NewIndexValue()
5252
enumerator.Initialize(headers);
5353

5454
var hpackEncoder = new DynamicHPackEncoder();
55-
Assert.True(HPackHeaderWriter.BeginEncodeHeaders(302, hpackEncoder, enumerator, buffer, out var length));
55+
Assert.Equal(HeaderWriteResult.Done, HPackHeaderWriter.BeginEncodeHeaders(302, hpackEncoder, enumerator, buffer, out var length));
5656

5757
var result = buffer.Slice(5, length - 5).ToArray();
5858
var hex = BitConverter.ToString(result);
@@ -81,7 +81,7 @@ public void BeginEncodeHeaders_MaxHeaderTableSizeExceeded_EvictionsToFit()
8181

8282
// First response
8383
enumerator.Initialize(headers);
84-
Assert.True(HPackHeaderWriter.BeginEncodeHeaders(302, hpackEncoder, enumerator, buffer, out var length));
84+
Assert.Equal(HeaderWriteResult.Done, HPackHeaderWriter.BeginEncodeHeaders(302, hpackEncoder, enumerator, buffer, out var length));
8585

8686
var result = buffer.Slice(0, length).ToArray();
8787
var hex = BitConverter.ToString(result);
@@ -123,7 +123,7 @@ public void BeginEncodeHeaders_MaxHeaderTableSizeExceeded_EvictionsToFit()
123123

124124
// Second response
125125
enumerator.Initialize(headers);
126-
Assert.True(HPackHeaderWriter.BeginEncodeHeaders(307, hpackEncoder, enumerator, buffer, out length));
126+
Assert.Equal(HeaderWriteResult.Done, HPackHeaderWriter.BeginEncodeHeaders(307, hpackEncoder, enumerator, buffer, out length));
127127

128128
result = buffer.Slice(0, length).ToArray();
129129
hex = BitConverter.ToString(result);
@@ -164,7 +164,7 @@ public void BeginEncodeHeaders_MaxHeaderTableSizeExceeded_EvictionsToFit()
164164
headers.SetCookie = "foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1";
165165

166166
enumerator.Initialize(headers);
167-
Assert.True(HPackHeaderWriter.BeginEncodeHeaders(200, hpackEncoder, enumerator, buffer, out length));
167+
Assert.Equal(HeaderWriteResult.Done, HPackHeaderWriter.BeginEncodeHeaders(200, hpackEncoder, enumerator, buffer, out length));
168168

169169
result = buffer.Slice(0, length).ToArray();
170170
hex = BitConverter.ToString(result);
@@ -225,7 +225,7 @@ public void BeginEncodeHeadersCustomEncoding_MaxHeaderTableSizeExceeded_Eviction
225225

226226
// First response
227227
enumerator.Initialize((HttpResponseHeaders)headers);
228-
Assert.True(HPackHeaderWriter.BeginEncodeHeaders(302, hpackEncoder, enumerator, buffer, out var length));
228+
Assert.Equal(HeaderWriteResult.Done, HPackHeaderWriter.BeginEncodeHeaders(302, hpackEncoder, enumerator, buffer, out var length));
229229

230230
var result = buffer.Slice(0, length).ToArray();
231231
var hex = BitConverter.ToString(result);
@@ -267,7 +267,7 @@ public void BeginEncodeHeadersCustomEncoding_MaxHeaderTableSizeExceeded_Eviction
267267

268268
// Second response
269269
enumerator.Initialize(headers);
270-
Assert.True(HPackHeaderWriter.BeginEncodeHeaders(307, hpackEncoder, enumerator, buffer, out length));
270+
Assert.Equal(HeaderWriteResult.Done, HPackHeaderWriter.BeginEncodeHeaders(307, hpackEncoder, enumerator, buffer, out length));
271271

272272
result = buffer.Slice(0, length).ToArray();
273273
hex = BitConverter.ToString(result);
@@ -308,7 +308,7 @@ public void BeginEncodeHeadersCustomEncoding_MaxHeaderTableSizeExceeded_Eviction
308308
headers.SetCookie = "foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1";
309309

310310
enumerator.Initialize(headers);
311-
Assert.True(HPackHeaderWriter.BeginEncodeHeaders(200, hpackEncoder, enumerator, buffer, out length));
311+
Assert.Equal(HeaderWriteResult.Done, HPackHeaderWriter.BeginEncodeHeaders(200, hpackEncoder, enumerator, buffer, out length));
312312

313313
result = buffer.Slice(0, length).ToArray();
314314
hex = BitConverter.ToString(result);
@@ -366,7 +366,7 @@ public void BeginEncodeHeaders_ExcludedHeaders_NotAddedToTable(string headerName
366366
enumerator.Initialize(headers);
367367

368368
var hpackEncoder = new DynamicHPackEncoder(maxHeaderTableSize: Http2PeerSettings.DefaultHeaderTableSize);
369-
Assert.True(HPackHeaderWriter.BeginEncodeHeaders(hpackEncoder, enumerator, buffer, out _));
369+
Assert.Equal(HeaderWriteResult.Done, HPackHeaderWriter.BeginEncodeHeaders(hpackEncoder, enumerator, buffer, out _));
370370

371371
if (neverIndex)
372372
{
@@ -392,7 +392,7 @@ public void BeginEncodeHeaders_HeaderExceedHeaderTableSize_NoIndexAndNoHeaderEnt
392392
enumerator.Initialize(headers);
393393

394394
var hpackEncoder = new DynamicHPackEncoder();
395-
Assert.True(HPackHeaderWriter.BeginEncodeHeaders(200, hpackEncoder, enumerator, buffer, out var length));
395+
Assert.Equal(HeaderWriteResult.Done, HPackHeaderWriter.BeginEncodeHeaders(200, hpackEncoder, enumerator, buffer, out var length));
396396

397397
Assert.Empty(GetHeaderEntries(hpackEncoder));
398398
}
@@ -482,11 +482,11 @@ public void EncodesHeadersInSinglePayloadWhenSpaceAvailable(KeyValuePair<string,
482482
var length = 0;
483483
if (statusCode.HasValue)
484484
{
485-
Assert.True(HPackHeaderWriter.BeginEncodeHeaders(statusCode.Value, hpackEncoder, GetHeadersEnumerator(headers), payload, out length));
485+
Assert.Equal(HeaderWriteResult.Done, HPackHeaderWriter.BeginEncodeHeaders(statusCode.Value, hpackEncoder, GetHeadersEnumerator(headers), payload, out length));
486486
}
487487
else
488488
{
489-
Assert.True(HPackHeaderWriter.BeginEncodeHeaders(hpackEncoder, GetHeadersEnumerator(headers), payload, out length));
489+
Assert.Equal(HeaderWriteResult.Done, HPackHeaderWriter.BeginEncodeHeaders(hpackEncoder, GetHeadersEnumerator(headers), payload, out length));
490490
}
491491
Assert.Equal(expectedPayload.Length, length);
492492

@@ -548,28 +548,28 @@ public void EncodesHeadersInMultiplePayloadsWhenSpaceNotAvailable(bool exactSize
548548

549549
// When !exactSize, slices are one byte short of fitting the next header
550550
var sliceLength = expectedStatusCodePayload.Length + (exactSize ? 0 : expectedDateHeaderPayload.Length - 1);
551-
Assert.False(HPackHeaderWriter.BeginEncodeHeaders(statusCode, hpackEncoder, headerEnumerator, payload.Slice(offset, sliceLength), out var length));
551+
Assert.Equal(HeaderWriteResult.MoreHeaders, HPackHeaderWriter.BeginEncodeHeaders(statusCode, hpackEncoder, headerEnumerator, payload.Slice(offset, sliceLength), out var length));
552552
Assert.Equal(expectedStatusCodePayload.Length, length);
553553
Assert.Equal(expectedStatusCodePayload, payload.Slice(0, length).ToArray());
554554

555555
offset += length;
556556

557557
sliceLength = expectedDateHeaderPayload.Length + (exactSize ? 0 : expectedContentTypeHeaderPayload.Length - 1);
558-
Assert.False(HPackHeaderWriter.ContinueEncodeHeaders(hpackEncoder, headerEnumerator, payload.Slice(offset, sliceLength), out length));
558+
Assert.Equal(HeaderWriteResult.MoreHeaders, HPackHeaderWriter.ContinueEncodeHeaders(hpackEncoder, headerEnumerator, payload.Slice(offset, sliceLength), out length));
559559
Assert.Equal(expectedDateHeaderPayload.Length, length);
560560
Assert.Equal(expectedDateHeaderPayload, payload.Slice(offset, length).ToArray());
561561

562562
offset += length;
563563

564564
sliceLength = expectedContentTypeHeaderPayload.Length + (exactSize ? 0 : expectedServerHeaderPayload.Length - 1);
565-
Assert.False(HPackHeaderWriter.ContinueEncodeHeaders(hpackEncoder, headerEnumerator, payload.Slice(offset, sliceLength), out length));
565+
Assert.Equal(HeaderWriteResult.MoreHeaders, HPackHeaderWriter.ContinueEncodeHeaders(hpackEncoder, headerEnumerator, payload.Slice(offset, sliceLength), out length));
566566
Assert.Equal(expectedContentTypeHeaderPayload.Length, length);
567567
Assert.Equal(expectedContentTypeHeaderPayload, payload.Slice(offset, length).ToArray());
568568

569569
offset += length;
570570

571571
sliceLength = expectedServerHeaderPayload.Length;
572-
Assert.True(HPackHeaderWriter.ContinueEncodeHeaders(hpackEncoder, headerEnumerator, payload.Slice(offset, sliceLength), out length));
572+
Assert.Equal(HeaderWriteResult.Done, HPackHeaderWriter.ContinueEncodeHeaders(hpackEncoder, headerEnumerator, payload.Slice(offset, sliceLength), out length));
573573
Assert.Equal(expectedServerHeaderPayload.Length, length);
574574
Assert.Equal(expectedServerHeaderPayload, payload.Slice(offset, length).ToArray());
575575
}
@@ -586,7 +586,7 @@ public void BeginEncodeHeaders_MaxHeaderTableSizeUpdated_SizeUpdateInHeaders()
586586

587587
// First request
588588
enumerator.Initialize(new Dictionary<string, StringValues>());
589-
Assert.True(HPackHeaderWriter.BeginEncodeHeaders(hpackEncoder, enumerator, buffer, out var length));
589+
Assert.Equal(HeaderWriteResult.Done, HPackHeaderWriter.BeginEncodeHeaders(hpackEncoder, enumerator, buffer, out var length));
590590

591591
Assert.Equal(2, length);
592592

@@ -600,11 +600,68 @@ public void BeginEncodeHeaders_MaxHeaderTableSizeUpdated_SizeUpdateInHeaders()
600600

601601
// Second request
602602
enumerator.Initialize(new Dictionary<string, StringValues>());
603-
Assert.True(HPackHeaderWriter.BeginEncodeHeaders(hpackEncoder, enumerator, buffer, out length));
603+
Assert.Equal(HeaderWriteResult.Done, HPackHeaderWriter.BeginEncodeHeaders(hpackEncoder, enumerator, buffer, out length));
604604

605605
Assert.Equal(0, length);
606606
}
607607

608+
[Fact]
609+
public void WithStatusCode_TooLargeHeader_ReturnsNotDone()
610+
{
611+
Span<byte> buffer = new byte[1024 * 16];
612+
613+
IHeaderDictionary headers = new HttpResponseHeaders();
614+
headers.Cookie = new string('a', buffer.Length + 1);
615+
var enumerator = new Http2HeadersEnumerator();
616+
enumerator.Initialize(headers);
617+
618+
var hpackEncoder = new DynamicHPackEncoder();
619+
Assert.Equal(HeaderWriteResult.MoreHeaders, HPackHeaderWriter.BeginEncodeHeaders(200, hpackEncoder, enumerator, buffer, out var length));
620+
}
621+
622+
[Fact]
623+
public void NoStatusCodeLargeHeader_ReturnsOversized()
624+
{
625+
Span<byte> buffer = new byte[1024 * 16];
626+
627+
IHeaderDictionary headers = new HttpResponseHeaders();
628+
headers.Cookie = new string('a', buffer.Length + 1);
629+
var enumerator = new Http2HeadersEnumerator();
630+
enumerator.Initialize(headers);
631+
632+
var hpackEncoder = new DynamicHPackEncoder();
633+
Assert.Equal(HeaderWriteResult.BufferTooSmall, HPackHeaderWriter.BeginEncodeHeaders(hpackEncoder, enumerator, buffer, out var length));
634+
}
635+
636+
[Fact]
637+
public void WithStatusCode_JustFittingHeaderNoSpace_ReturnsNotDone()
638+
{
639+
Span<byte> buffer = new byte[1024 * 16];
640+
641+
IHeaderDictionary headers = new HttpResponseHeaders();
642+
headers.Cookie = new string('a', buffer.Length - 1);
643+
var enumerator = new Http2HeadersEnumerator();
644+
enumerator.Initialize(headers);
645+
646+
var hpackEncoder = new DynamicHPackEncoder();
647+
Assert.Equal(HeaderWriteResult.MoreHeaders, HPackHeaderWriter.BeginEncodeHeaders(200, hpackEncoder, enumerator, buffer, out var length));
648+
}
649+
650+
[Fact]
651+
public void NoStatusCode_JustFittingHeaderNoSpace_ReturnsNotDone()
652+
{
653+
Span<byte> buffer = new byte[1024 * 16];
654+
655+
IHeaderDictionary headers = new HttpResponseHeaders();
656+
headers.Accept = "application/json;";
657+
headers.Cookie = new string('a', buffer.Length - 1);
658+
var enumerator = new Http2HeadersEnumerator();
659+
enumerator.Initialize(headers);
660+
661+
var hpackEncoder = new DynamicHPackEncoder();
662+
Assert.Equal(HeaderWriteResult.MoreHeaders, HPackHeaderWriter.BeginEncodeHeaders(hpackEncoder, enumerator, buffer, out var length));
663+
}
664+
608665
private static Http2HeadersEnumerator GetHeadersEnumerator(IEnumerable<KeyValuePair<string, string>> headers)
609666
{
610667
var groupedHeaders = headers

0 commit comments

Comments
 (0)