PSA: Take care with settings snapshots in HTTP/2

I was just re-reviewing something for WebTransport and I've opened an issue on that spec relating to how it is a little bit sloppy when it comes to how settings interact with session creation:

https://github.com/ietf-wg-webtrans/draft-ietf-webtrans-http2/issues/189

I'm not going to draw pictures for this email, but you are welcome to read that issue to get a sense of what is going on.

In short, taking a snapshot of HTTP/2 settings is risky.

In HTTP/2, we treat stream opening as something of an atomic operation.  It's not, but it's a convenient fiction in our minds that is helped somewhat by the in-order guarantees from the underlying transport that HTTP/2 uses.  HTTP/2 itself either handles or sidesteps any issues that arise from there being two parties involved, which includes the somewhat quirky design of SETTINGS_INITIAL_WINDOW design, which doesn't rely on a snapshot of settings.  (After spending so much time with QUIC, this design seems a bit weird, but it is sound.)

HTTP/3 avoids this problem entirely by having just one SETTINGS frame that can't change.

What motivates this PSA is that this casual handling of stream opens encourages laziness elsewhere, when the necessary assumptions don't hold.  We might like to take a snapshot of settings for various purposes and it seems easy to do.  However, there is a race condition you need to be aware of and handle properly.  This is what happened with WebTransport, where we don't seem to be handling this well.

In order to use settings safely for things related to individual streams, you have two choices: either the value needs to be updated with every settings change (as SETTINGS_INITIAL_WINDOW does) or you take a snapshot. A snapshot can seem appealing, but correct handling is subtle and tricky.

There is no way to guarantee ordering for any other interaction if snapshots are imprecisely defined with language like "when a request is created".  This is because a request being created is not well-defined concept: there is no single point in time that both client and server can agree on.

The only[*1] safe way to take a snapshot of HTTP/2 settings is:

1. The settings that apply to the client are those that the client acknowledged prior to sending the request; and
2. The settings that apply to the server are those that the server acknowledged prior to sending the response.

The HEADERS frame that starts a stream is the most logical point to anchor on in both cases.  The [*1] is to acknowledge that this is obviously not the only option. You could create more elaborate dependency interactions with other landmark points in request handling, but I'm going to recommend that we avoid doing anything more elaborate.  Anyone who wants that is asking for a whole bag full of footguns.

What could be surprising about this arrangement is that, while servers can take a snapshot at the time they write[*2] a response, taking a snapshot is not an atomic operation for clients.  If the client snapshots settings when creating any state associated with a request, the server settings need to be updated at the point the response is received.  Otherwise, the client and server will disagree.

Note [*2] above.  Both client and server need to make the snapshot when the HEADERS frame is written (or otherwise committed to a write buffer).  If you have some sort of asynchronous processing involved such that settings might be acknowledged between when the processing occurs and when the frame is written, then you will also need to work out how to asynchronously ensure the snapshot matches the state of the acknowledged settings when the frame is written.  I wish anyone who seeks to play with asynchronous programming the best of luck (https://bholley.net/blog/2015/must-be-this-tall-to-write-multi-threaded-code.html).

The first instance I found where this problem might have had any effect is RFC 8441.  A snapshot of the value of SETTINGS_ENABLE_CONNECT_PROTOCOL is effectively taken when an extended CONNECT request is made.

Thankfully, SETTINGS_ENABLE_CONNECT_PROTOCOL cannot transition from enabled (1) to disabled (0), so this is a bit of a theoretical problem only. Though the transition to an enabled state potentially creates this sort of race condition ambiguity, the setting only really affects whether a client can safely make a request, which they can only do once the setting is set to the enabled state and they can always do thereafter.  Critically, this doesn't affect any handling of the response, which is where special handling is likely necessary.

This is obviously far more consequential for WebTransport, which very much depends on the snapshot. We'll probably have exemplary language about what to do with this problem once we get through with fixing this issue there.

Thanks,
Martin

Received on Tuesday, 16 June 2026 06:50:56 UTC