@@ -29,6 +29,8 @@ internal sealed class Http2FrameWriter
2929 /// TODO (https://github.com/dotnet/aspnetcore/issues/51309): eliminate this limit.
3030 private const string MaximumFlowControlQueueSizeProperty = "Microsoft.AspNetCore.Server.Kestrel.Http2.MaxConnectionFlowControlQueueSize" ;
3131
32+ private const int HeaderBufferSizeMultiplier = 2 ;
33+
3234 private static readonly int ? AppContextMaximumFlowControlQueueSize = GetAppContextMaximumFlowControlQueueSize ( ) ;
3335
3436 private static int ? GetAppContextMaximumFlowControlQueueSize ( )
@@ -71,8 +73,12 @@ internal sealed class Http2FrameWriter
7173 // This is only set to true by tests.
7274 private readonly bool _scheduleInline ;
7375
74- private uint _maxFrameSize = Http2PeerSettings . MinAllowedMaxFrameSize ;
76+ private int _maxFrameSize = Http2PeerSettings . MinAllowedMaxFrameSize ;
7577 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 ;
7682 private long _unflushedBytes ;
7783
7884 private bool _completed ;
@@ -110,7 +116,6 @@ public Http2FrameWriter(
110116 _headerEncodingBuffer = new byte [ _maxFrameSize ] ;
111117
112118 _scheduleInline = serviceContext . Scheduler == PipeScheduler . Inline ;
113-
114119 _hpackEncoder = new DynamicHPackEncoder ( serviceContext . ServerOptions . AllowResponseHeaderCompression ) ;
115120
116121 _maximumFlowControlQueueSize = AppContextMaximumFlowControlQueueSize is null
@@ -367,12 +372,15 @@ public void UpdateMaxHeaderTableSize(uint maxHeaderTableSize)
367372 }
368373 }
369374
370- public void UpdateMaxFrameSize ( uint maxFrameSize )
375+ public void UpdateMaxFrameSize ( int maxFrameSize )
371376 {
372377 lock ( _writeLock )
373378 {
374379 if ( _maxFrameSize != maxFrameSize )
375380 {
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 ) ;
376384 _maxFrameSize = maxFrameSize ;
377385 _headerEncodingBuffer = new byte [ _maxFrameSize ] ;
378386 }
@@ -507,11 +515,12 @@ private void WriteResponseHeadersUnsynchronized(int streamId, int statusCode, Ht
507515 {
508516 try
509517 {
518+ // In the case of the headers, there is always a status header to be returned, so BeginEncodeHeaders will not return BufferTooSmall.
510519 _headersEnumerator . Initialize ( headers ) ;
511520 _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 ) ;
515524 }
516525 // Any exception from the HPack encoder can leave the dynamic table in a corrupt state.
517526 // 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
548557
549558 try
550559 {
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.
552561 _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 ) ;
556565 }
557566 // Any exception from the HPack encoder can leave the dynamic table in a corrupt state.
558567 // 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
566575 }
567576 }
568577
569- private void FinishWritingHeadersUnsynchronized ( int streamId , int payloadLength , bool done )
578+ private void SplitHeaderAcrossFrames ( int streamId , ReadOnlySpan < byte > dataToFrame , bool endOfHeaders , bool isFramePrepared )
570579 {
571- var buffer = _headerEncodingBuffer . AsSpan ( ) ;
572- _outgoingFrame . PayloadLength = payloadLength ;
573- if ( done )
580+ var shouldPrepareFrame = ! isFramePrepared ;
581+ while ( dataToFrame . Length > 0 )
574582 {
575- _outgoingFrame . HeadersFlags |= Http2HeadersFrameFlags . END_HEADERS ;
576- }
583+ if ( shouldPrepareFrame )
584+ {
585+ _outgoingFrame . PrepareContinuation ( Http2ContinuationFrameFlags . NONE , streamId ) ;
586+ }
577587
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+ }
580596
581- while ( ! done )
582- {
583- _outgoingFrame . PrepareContinuation ( Http2ContinuationFrameFlags . NONE , streamId ) ;
597+ WriteHeaderUnsynchronized ( ) ;
598+ _outputWriter . Write ( dataToFrame [ ..currentSize ] ) ;
599+ dataToFrame = dataToFrame . Slice ( currentSize ) ;
600+ }
601+ }
584602
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.
586611 _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 )
589645 {
590- _outgoingFrame . ContinuationFlags = Http2ContinuationFrameFlags . END_HEADERS ;
646+ return ;
591647 }
648+ }
592649
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 ) ;
595674 }
596675 }
597676
@@ -1023,4 +1102,4 @@ private void EnqueueWaitingForMoreConnectionWindow(Http2OutputProducer producer)
10231102 _http2Connection . Abort ( new ConnectionAbortedException ( "HTTP/2 connection exceeded the outgoing flow control maximum queue size." ) ) ;
10241103 }
10251104 }
1026- }
1105+ }
0 commit comments