Proposal: enable an HTTP/2 header-block read timeout
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 1.6k
- Forks
- 382
- Avg merge
- 20h 23m
- Merged PRs (30d)
- 9
Description
Proposal: enable an HTTP/2 header block read timeout
Currently there is a read timeout for http1 in Hyper: http1::Builder::header_read_timeout,
and no such http/2 equivalent exists. This proposal aims on adding this equivalent to hyper enabled via h2 and is based on the following issues:
-
Relevant: https://github.com/tokio-rs/axum/issues/2741
- Workaround proposed: https://gist.github.com/programatik29/36d371c657392fd7f322e7342957b6d1
- Closing comment: https://github.com/tokio-rs/axum/issues/2741#issuecomment-2131381768
-
Hyper definitely cares about DOS attacks AFAIK. I don't know why this one seems low(er) priority. But anyways, the more I think about it the less I see why axum would try to do anything here, since it can be solved more easily in hyper.
Because of that, I'll close this. I think it would be best for the discussion to continue on the hyper issue.
-
Direct: https://github.com/hyperium/h2/issues/823 (This is the intended proposal for this issue, as asked here)
One of the main concerns that this proposal and the issues highlighted above bring to light is that a server cannot authenticate a request until its headers are decoded. So a client can retain an unauthenticated connection by sending a valid header frame or header block indefinitely slowly which becomes the HTTP/2 form of a Slowloris attack.
Refer:
The LengthDelimitedCodec usage in h2 buffers input until a complete physical frame is available. FramedRead::decode then assembles a logical header/field block (in h2 referred as header block, see https://github.com/hyperium/h2/blob/master/src/frame/headers.rs#L19-L35) across HEADERS and CONTINUATION frames until END_HEADERS. The existing limits bound: frame size, header-list size, and continuation count and not how long the peer may take to send them. Because a continuation sequence cannot be interleaved with frames from other streams, a single incomplete header block stalls parsing for the entire connection!
Proposal
- As suggested by @seanmonstar in https://github.com/hyperium/h2/issues/826#issuecomment-2561115601 that h2 should avoid tokio runtime based stuff such as timers
- Further in https://github.com/hyperium/h2/issues/826#issuecomment-2561151660, @seanmonstar suggested doing a
sleep()and check on active streams, and exposing relevant state for it in h2 as done in https://github.com/hyperium/h2/pull/838 - As mentioned by @seanmonstar in https://github.com/hyperium/h2/issues/731#issuecomment-1881839229, the defaults are better selected by Hyper than by the protocol crate.
Given the above, I propose to have h2 expose the protocol progress needed to enforce a timeout. This can be something like:
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct HeaderBlockId(u64);
// https://github.com/hyperium/h2/blob/master/src/server.rs
impl<T, B: Buf> Connection<T, B> {
pub fn incomplete_header_block(&self) -> Option<HeaderBlockId>;
}
The HeaderBlockId token has the following semantics:
-
Some(id)means h2 may be reading an inbound header block. The ID starts provisionally with the frame prefix, remains active acrossCONTINUATIONframes, and clears onceEND_HEADERSis fully decoded or the frame is identified as non-header. -
Each new header block receives a different opaque ID, allowing Hyper to distinguish the blocks.
An ID is used instead of a boolean because h2 may complete one block and begin another. a changed ID tells Hyper to start a new deadline. FramedRead tracks the ID as it buffers input and partial header block. server::Connection exposes it to Hyper, which manages and enforces the timer/deadline.
Hyper integration
Hyper exposes a http2::Builder::timer. And we can add an HTTP/1 style option:
pub fn header_read_timeout(
&mut self,
timeout: impl Into<Option<Duration>>,
) -> &mut Self;
Store the current header block ID and its deadline in Hyper’s serving state. The current loop waits directly on poll_accept. instead, after each call:
- No ID: cancel the deadline
- Same ID: keep the existing deadline
- New ID: start a new deadline
| Previous deadline | h2 returns | Hyper action |
|---|---|---|
None |
Some(A) |
Start A’s deadline |
A |
Some(A) |
Keep A’s deadline |
A |
None |
A completed cancel the deadline |
A |
Some(B) |
A completed and B started, start B’s deadline |
The deadline is fixed i.e. receiving more header bytes does not reset it. If the header block completes during the poll, cancel its deadline before checking expiry. On expiry we terminate the entire connection and return an error for which Error::is_timeout() is true. The timeout closes the entire connection because an unfinished header block prevents h2 from processing any later frames on it. Hyper should make a best effort attempt to notify the client with GOAWAY(ENHANCE_YOUR_CALM) and then close immediately.
Notes
-
This timeout covers inbound http/2 request headers. Unlike http/1’s timeout, it does not cover idle connections, TLS or http/2 setup etc - those require separate timeouts. Thus, this does not cover the no request case discussed in https://github.com/tokio-rs/axum/issues/2741.
-
Other interesting implementations I looked into: NGINX, Envoy, Apache Traffic Server, haproxy and nghttpx which all provide a form of timeout for the case described.
-
I used GPT 5.6 to help me understand the code and debate/brainstorm my thought process through adversarial review and Cursor Grok 4.5 to help me navigate and mind map the codebase.
Thanks for all the work on h2! Please correct me if I went wrong somewhere in my understanding, I would be happy to make any changes/collaborate and drive this myself inclduing relevant PRs for hyper and h2.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with src/codec/mod.rs, src/frame/headers.rs, and src/server.rs to trace how HEADERS and CONTINUATION frames are buffered and exposed. Review the h2 issue and referenced Hyper integration points before settling the API shape. Done means h2 exposes reliable header-block progress that Hyper can use to enforce a fixed inbound header timeout.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- networking
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 38/100