@@ -29,6 +29,8 @@ internal sealed class Http2FrameWriter
29
29
/// TODO (https://github.com/dotnet/aspnetcore/issues/51309): eliminate this limit.
30
30
private const string MaximumFlowControlQueueSizeProperty = "Microsoft.AspNetCore.Server.Kestrel.Http2.MaxConnectionFlowControlQueueSize" ;
31
31
32
+ private const int HeaderBufferSizeMultiplier = 2 ;
33
+
32
34
private static readonly int ? AppContextMaximumFlowControlQueueSize = GetAppContextMaximumFlowControlQueueSize ( ) ;
33
35
34
36
private static int ? GetAppContextMaximumFlowControlQueueSize ( )
@@ -71,8 +73,12 @@ internal sealed class Http2FrameWriter
71
73
// This is only set to true by tests.
72
74
private readonly bool _scheduleInline ;
73
75
74
- private uint _maxFrameSize = Http2PeerSettings . MinAllowedMaxFrameSize ;
76
+ private int _maxFrameSize = Http2PeerSettings . MinAllowedMaxFrameSize ;
75
77
private byte [ ] _headerEncodingBuffer ;
78
+
79
+ // Keep track of the high-water mark of _headerEncodingBuffer's size so we don't have to grow
80
+ // through intermediate sizes repeatedly.
81
+ private int _headersEncodingLargeBufferSize = Http2PeerSettings . MinAllowedMaxFrameSize * HeaderBufferSizeMultiplier ;
76
82
private long _unflushedBytes ;
77
83
78
84
private bool _completed ;
@@ -110,7 +116,6 @@ public Http2FrameWriter(
110
116
_headerEncodingBuffer = new byte [ _maxFrameSize ] ;
111
117
112
118
_scheduleInline = serviceContext . Scheduler == PipeScheduler . Inline ;
113
-
114
119
_hpackEncoder = new DynamicHPackEncoder ( serviceContext . ServerOptions . AllowResponseHeaderCompression ) ;
115
120
116
121
_maximumFlowControlQueueSize = AppContextMaximumFlowControlQueueSize is null
@@ -367,12 +372,15 @@ public void UpdateMaxHeaderTableSize(uint maxHeaderTableSize)
367
372
}
368
373
}
369
374
370
- public void UpdateMaxFrameSize ( uint maxFrameSize )
375
+ public void UpdateMaxFrameSize ( int maxFrameSize )
371
376
{
372
377
lock ( _writeLock )
373
378
{
374
379
if ( _maxFrameSize != maxFrameSize )
375
380
{
381
+ // Safe multiply, MaxFrameSize is limited to 2^24-1 bytes by the protocol and by Http2PeerSettings.
382
+ // Ref: https://datatracker.ietf.org/doc/html/rfc7540#section-4.2
383
+ _headersEncodingLargeBufferSize = int . Max ( _headersEncodingLargeBufferSize , maxFrameSize * HeaderBufferSizeMultiplier ) ;
376
384
_maxFrameSize = maxFrameSize ;
377
385
_headerEncodingBuffer = new byte [ _maxFrameSize ] ;
378
386
}
@@ -507,11 +515,12 @@ private void WriteResponseHeadersUnsynchronized(int streamId, int statusCode, Ht
507
515
{
508
516
try
509
517
{
518
+ // In the case of the headers, there is always a status header to be returned, so BeginEncodeHeaders will not return BufferTooSmall.
510
519
_headersEnumerator . Initialize ( headers ) ;
511
520
_outgoingFrame . PrepareHeaders ( headerFrameFlags , streamId ) ;
512
- var buffer = _headerEncodingBuffer . AsSpan ( ) ;
513
- var done = HPackHeaderWriter . BeginEncodeHeaders ( statusCode , _hpackEncoder , _headersEnumerator , buffer , out var payloadLength ) ;
514
- FinishWritingHeadersUnsynchronized ( streamId , payloadLength , done ) ;
521
+ var writeResult = HPackHeaderWriter . BeginEncodeHeaders ( statusCode , _hpackEncoder , _headersEnumerator , _headerEncodingBuffer , out var payloadLength ) ;
522
+ Debug . Assert ( writeResult != HeaderWriteResult . BufferTooSmall , "This always writes the status as the first header, and it should never be an over the buffer size." ) ;
523
+ FinishWritingHeadersUnsynchronized ( streamId , payloadLength , writeResult ) ;
515
524
}
516
525
// Any exception from the HPack encoder can leave the dynamic table in a corrupt state.
517
526
// Since we allow custom header encoders we don't know what type of exceptions to expect.
@@ -548,11 +557,11 @@ private ValueTask<FlushResult> WriteDataAndTrailersAsync(Http2Stream stream, in
548
557
549
558
try
550
559
{
551
- _headersEnumerator . Initialize ( headers ) ;
560
+ // In the case of the trailers, there is no status header to be written, so even the first call to BeginEncodeHeaders can return BufferTooSmall.
552
561
_outgoingFrame . PrepareHeaders ( Http2HeadersFrameFlags . END_STREAM , streamId ) ;
553
- var buffer = _headerEncodingBuffer . AsSpan ( ) ;
554
- var done = HPackHeaderWriter . BeginEncodeHeaders ( _hpackEncoder , _headersEnumerator , buffer , out var payloadLength ) ;
555
- FinishWritingHeadersUnsynchronized ( streamId , payloadLength , done ) ;
562
+ _headersEnumerator . Initialize ( headers ) ;
563
+ var writeResult = HPackHeaderWriter . BeginEncodeHeaders ( _hpackEncoder , _headersEnumerator , _headerEncodingBuffer , out var payloadLength ) ;
564
+ FinishWritingHeadersUnsynchronized ( streamId , payloadLength , writeResult ) ;
556
565
}
557
566
// Any exception from the HPack encoder can leave the dynamic table in a corrupt state.
558
567
// Since we allow custom header encoders we don't know what type of exceptions to expect.
@@ -566,32 +575,102 @@ private ValueTask<FlushResult> WriteDataAndTrailersAsync(Http2Stream stream, in
566
575
}
567
576
}
568
577
569
- private void FinishWritingHeadersUnsynchronized ( int streamId , int payloadLength , bool done )
578
+ private void SplitHeaderAcrossFrames ( int streamId , ReadOnlySpan < byte > dataToFrame , bool endOfHeaders , bool isFramePrepared )
570
579
{
571
- var buffer = _headerEncodingBuffer . AsSpan ( ) ;
572
- _outgoingFrame . PayloadLength = payloadLength ;
573
- if ( done )
580
+ var shouldPrepareFrame = ! isFramePrepared ;
581
+ while ( dataToFrame . Length > 0 )
574
582
{
575
- _outgoingFrame . HeadersFlags |= Http2HeadersFrameFlags . END_HEADERS ;
576
- }
583
+ if ( shouldPrepareFrame )
584
+ {
585
+ _outgoingFrame . PrepareContinuation ( Http2ContinuationFrameFlags . NONE , streamId ) ;
586
+ }
577
587
578
- WriteHeaderUnsynchronized ( ) ;
579
- _outputWriter . Write ( buffer . Slice ( 0 , payloadLength ) ) ;
588
+ // Should prepare continuation frames.
589
+ shouldPrepareFrame = true ;
590
+ var currentSize = Math . Min ( dataToFrame . Length , _maxFrameSize ) ;
591
+ _outgoingFrame . PayloadLength = currentSize ;
592
+ if ( endOfHeaders && dataToFrame . Length == currentSize )
593
+ {
594
+ _outgoingFrame . HeadersFlags |= Http2HeadersFrameFlags . END_HEADERS ;
595
+ }
580
596
581
- while ( ! done )
582
- {
583
- _outgoingFrame . PrepareContinuation ( Http2ContinuationFrameFlags . NONE , streamId ) ;
597
+ WriteHeaderUnsynchronized ( ) ;
598
+ _outputWriter . Write ( dataToFrame [ ..currentSize ] ) ;
599
+ dataToFrame = dataToFrame . Slice ( currentSize ) ;
600
+ }
601
+ }
584
602
585
- done = HPackHeaderWriter . ContinueEncodeHeaders ( _hpackEncoder , _headersEnumerator , buffer , out payloadLength ) ;
603
+ private void FinishWritingHeadersUnsynchronized ( int streamId , int payloadLength , HeaderWriteResult writeResult )
604
+ {
605
+ Debug . Assert ( payloadLength <= _maxFrameSize , "The initial payload lengths is written to _headerEncodingBuffer with size of _maxFrameSize" ) ;
606
+ byte [ ] ? largeHeaderBuffer = null ;
607
+ Span < byte > buffer ;
608
+ if ( writeResult == HeaderWriteResult . Done )
609
+ {
610
+ // Fast path, only a single HEADER frame.
586
611
_outgoingFrame . PayloadLength = payloadLength ;
587
-
588
- if ( done )
612
+ _outgoingFrame . HeadersFlags |= Http2HeadersFrameFlags . END_HEADERS ;
613
+ WriteHeaderUnsynchronized ( ) ;
614
+ _outputWriter . Write ( _headerEncodingBuffer . AsSpan ( 0 , payloadLength ) ) ;
615
+ return ;
616
+ }
617
+ else if ( writeResult == HeaderWriteResult . MoreHeaders )
618
+ {
619
+ _outgoingFrame . PayloadLength = payloadLength ;
620
+ WriteHeaderUnsynchronized ( ) ;
621
+ _outputWriter . Write ( _headerEncodingBuffer . AsSpan ( 0 , payloadLength ) ) ;
622
+ }
623
+ else
624
+ {
625
+ // This may happen in case of the TRAILERS after the initial encode operation.
626
+ // The _maxFrameSize sized _headerEncodingBuffer was too small.
627
+ while ( writeResult == HeaderWriteResult . BufferTooSmall )
628
+ {
629
+ Debug . Assert ( payloadLength == 0 , "Payload written even though buffer is too small" ) ;
630
+ largeHeaderBuffer = ArrayPool < byte > . Shared . Rent ( _headersEncodingLargeBufferSize ) ;
631
+ buffer = largeHeaderBuffer . AsSpan ( 0 , _headersEncodingLargeBufferSize ) ;
632
+ writeResult = HPackHeaderWriter . RetryBeginEncodeHeaders ( _hpackEncoder , _headersEnumerator , buffer , out payloadLength ) ;
633
+ if ( writeResult != HeaderWriteResult . BufferTooSmall )
634
+ {
635
+ SplitHeaderAcrossFrames ( streamId , buffer [ ..payloadLength ] , endOfHeaders : writeResult == HeaderWriteResult . Done , isFramePrepared : true ) ;
636
+ }
637
+ else
638
+ {
639
+ _headersEncodingLargeBufferSize = checked ( _headersEncodingLargeBufferSize * HeaderBufferSizeMultiplier ) ;
640
+ }
641
+ ArrayPool < byte > . Shared . Return ( largeHeaderBuffer ) ;
642
+ largeHeaderBuffer = null ;
643
+ }
644
+ if ( writeResult == HeaderWriteResult . Done )
589
645
{
590
- _outgoingFrame . ContinuationFlags = Http2ContinuationFrameFlags . END_HEADERS ;
646
+ return ;
591
647
}
648
+ }
592
649
593
- WriteHeaderUnsynchronized ( ) ;
594
- _outputWriter . Write ( buffer . Slice ( 0 , payloadLength ) ) ;
650
+ // HEADERS and zero or more CONTINUATIONS sent - all subsequent frames are (unprepared) CONTINUATIONs
651
+ buffer = _headerEncodingBuffer ;
652
+ while ( writeResult != HeaderWriteResult . Done )
653
+ {
654
+ writeResult = HPackHeaderWriter . ContinueEncodeHeaders ( _hpackEncoder , _headersEnumerator , buffer , out payloadLength ) ;
655
+ if ( writeResult == HeaderWriteResult . BufferTooSmall )
656
+ {
657
+ if ( largeHeaderBuffer != null )
658
+ {
659
+ ArrayPool < byte > . Shared . Return ( largeHeaderBuffer ) ;
660
+ _headersEncodingLargeBufferSize = checked ( _headersEncodingLargeBufferSize * HeaderBufferSizeMultiplier ) ;
661
+ }
662
+ largeHeaderBuffer = ArrayPool < byte > . Shared . Rent ( _headersEncodingLargeBufferSize ) ;
663
+ buffer = largeHeaderBuffer . AsSpan ( 0 , _headersEncodingLargeBufferSize ) ;
664
+ }
665
+ else
666
+ {
667
+ // In case of Done or MoreHeaders: write to output.
668
+ SplitHeaderAcrossFrames ( streamId , buffer [ ..payloadLength ] , endOfHeaders : writeResult == HeaderWriteResult . Done , isFramePrepared : false ) ;
669
+ }
670
+ }
671
+ if ( largeHeaderBuffer != null )
672
+ {
673
+ ArrayPool < byte > . Shared . Return ( largeHeaderBuffer ) ;
595
674
}
596
675
}
597
676
@@ -1023,4 +1102,4 @@ private void EnqueueWaitingForMoreConnectionWindow(Http2OutputProducer producer)
1023
1102
_http2Connection . Abort ( new ConnectionAbortedException ( "HTTP/2 connection exceeded the outgoing flow control maximum queue size." ) ) ;
1024
1103
}
1025
1104
}
1026
- }
1105
+ }
0 commit comments