Large HTTP/2 Header Frames
10/19/2024 | 3 minutes to read
In a previous post I explored how HTTP2 handles header frames in .NET 8. In this post I explore how it is planned to handle larger HTTP2 response header values.
Recap
RFC7540 describes the HTTP/2 protocol's related details.
In HTTP/2 a request-response pair is serialized in a stream.
A stream consists of message frames.
Frames have a type, frame header, a given size and corresponding data. Frames are associated with a given stream with the stream ID.
HTTP/2 requests start with HEADER frame. A header frame may be followed by CONTINUATION frames containing further headers.
The HTTP/2 request headers HPack encoded and split into HEADER and CONTINUATION frames.
In ASP.NET Core's Kestrel
HPackHeaderWriterstatic class writes the headers.HPackHeaderWriteriterates over the headers/trailers (except for the response status header which is written separately) using theHttp2FrameWritertypes.
Http2FrameWritercreates the buffer which it passes as aSpan<byte>to the header writer. The default size of the buffer is 16K, but it can be updated via Kestrel's option by setting theHttp2Limits's MaxFrameSize property. Clients can also influence the frame size by sending a value in the SETTING frame.
Planned changes
HPackHeaderWritercan return 3 types of responses:all headers written (
Done)there are more headers to write (
MoreHeaders)could not write any header because the provided buffer is too small (
BufferToSmall)
Http2FrameWriterunderstands these returned values. Its behavior remains when a header can be written or more headers can be written as before. By default a frame sized buffer is allocated by every writer at objection construction (each connection creates a newHttp2FrameWriter). In thee cases, the buffer fits into a single frame, hence the buffer can be directly flushed to output in HEADER and following CONTINUATION frames.When
BufferToSmallresponse is returned,Http2FrameWritertries to rent a large buffer fromArrayPool<byte>.Shared. When the buffer is too small it retries by doubling the requested buffer size.Writing the response headers and trailers consists of 2 actions:
First, writing the initial HEADER frame. In this case writing to the buffer always succeeds with
MoreHeadersorDonebecause the status code is always written. This is handled as a fast-path. Notice, that when writing the trailers,HPackHeaderWritercan returnBufferToSmalleven for the first frame in the case the trailers collection only contains a single large value.
Once the initial frame is written to a buffer (and flushed to the output), the remaining values are written by the
FinishWritingHeadersUnsynchronizedmethod.
FinishWritingHeadersUnsynchronized increasing the temporary buffer size:
while (writeResult != HPackHeaderWriter.HeaderWriteResult.Done) { writeResult = HPackHeaderWriter.ContinueEncodeHeaders(_hpackEncoder, _headersEnumerator, buffer, out payloadLength); if (writeResult == HPackHeaderWriter.HeaderWriteResult.BufferTooSmall) { // Return the array that was rented in the previous iteration of the loop. if (largeHeaderBuffer != null) { ArrayPool<byte>.Shared.Return(largeHeaderBuffer); // Increases the buffer size 👇 Avoiding overflow. _headersEncodingLargeBufferSize = checked(_headersEncodingLargeBufferSize * HeaderBufferSizeMultiplier); } // Rents a large buffer 👇 largeHeaderBuffer = ArrayPool<byte>.Shared.Rent(_headersEncodingLargeBufferSize); buffer = largeHeaderBuffer.AsSpan(0, _headersEncodingLargeBufferSize); } else { // Split a large buffer into frames 👇 // In case of Done or MoreHeaders: write to output. SplitHeaderAcrossFrames(streamId, buffer[..payloadLength], endOfHeaders: writeResult == HPackHeaderWriter.HeaderWriteResult.Done, isFramePrepared: false); } } if (largeHeaderBuffer != null) { ArrayPool<byte>.Shared.Return(largeHeaderBuffer); }
When the headers are successfully written into the buffer,
SplitHeaderAcrossFramessplits this buffer to_maxFrameSizesized frames. This method also prepares the CONTINUATION frames.