Skip to content

Proposal: enable an HTTP/2 header-block read timeout #928

Description

@infiniteregrets

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:

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

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 across CONTINUATION frames, and clears once END_HEADERS is 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 How to avoid Slowloris DoS Attack? tokio-rs/axum#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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions