Large HTTP/2 Header Frames

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 HPackHeaderWriter static class writes the headers.

    • HPackHeaderWriter iterates over the headers/trailers (except for the response status header which is written separately) using the Http2FrameWriter types.

  • Http2FrameWriter creates the buffer which it passes as a Span<byte> to the header writer. The default size of the buffer is 16K, but it can be updated via Kestrel's option by setting the Http2Limits's MaxFrameSize property. Clients can also influence the frame size by sending a value in the SETTING frame.

Planned changes

  • HPackHeaderWriter can return 3 types of responses:

    1. all headers written (Done)

    2. there are more headers to write (MoreHeaders)

    3. could not write any header because the provided buffer is too small (BufferToSmall)

  • Http2FrameWriter understands 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 new Http2FrameWriter). 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.

  • WhenBufferToSmall response is returned, Http2FrameWriter tries to rent a large buffer from ArrayPool<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 MoreHeaders or Done because the status code is always written. This is handled as a fast-path. Notice, that when writing the trailers, HPackHeaderWriter can return BufferToSmall even 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 FinishWritingHeadersUnsynchronized method.

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, SplitHeaderAcrossFrames splits this buffer to _maxFrameSize sized frames. This method also prepares the CONTINUATION frames.