# 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](https://datatracker.ietf.org/doc/html/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](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.http2limits.maxframesize?view=aspnetcore-8.0) 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`)
  1. there are more headers to write (`MoreHeaders`)
  1. 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.
- When`BufferToSmall` 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:

```csharp
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.