Repository navigation
Add an overall upstream response-header deadline distinct from read_timeout #992
Description
Activity
seonghobae commented on Sep 3, 2026
Source pass against current protected main / pinned 09696b51bc59315353d96686355861604d0bb48c narrows the protocol semantics for this request:
- H1
HttpSession::read_response()keeps partial bytes inresponse_header_read_buf, reparses them after cancellation/resume, and appliesread_timeoutaround each individualread_buf(). A successful partial read therefore refreshes the inactivity timer. The same method can be called repeatedly for informational responses. - H2 is materially different today:
Http2Session::read_response_header()appliesread_timeoutonce aroundpoll_response_header(). It also has an explicit TODO for 1xx support, so the H1 byte-drip failure mode should not be mechanically projected onto current H2 behavior. PeerOptions::read_timeoutis plumbed independently into the H1/H2 sessions by the proxy paths. Reinterpreting it as the new overall deadline would therefore change an existing inactivity-timeout contract.
A cancellation-safe implementation appears to need a separate, monotonic response-header deadline stored with the response-header sequence, rather than constructing a fresh relative timeout on every read_response() call. For H1 that lets partial parser state survive ordinary task cancellation without allowing successful reads—or repeated 1xx responses—to reset the absolute budget. The deadline should be cleared only when the final non-informational response header is accepted or the request is abandoned/failed. This also makes the 1xx policy explicit: one budget from the first response-header wait through the final non-1xx header, rather than one new budget per informational response.
Suggested acceptance cases for the supplier change:
- H1 origin sends an incomplete header in fragments where every fragment arrives before
read_timeout, but total header time exceeds the new deadline: fail at the overall deadline. - Same H1 traffic with the new option unset: preserve current per-read behavior.
- H1 cancellation/resume before expiry: preserve accumulated bytes and the original absolute deadline.
- H1 informational-response sequence: 1xx responses do not refresh the deadline for the eventual final response.
- H2 final-response wait: the new option is independently enforced without changing existing
read_timeout; document the current lack of 1xx support rather than pretending H1/H2 parser behavior is identical. - Completed header inside both budgets remains unchanged.
I would keep the exact public field/API name open until the owner decides where it belongs, but semantically it should remain distinct from read_timeout and total_connection_timeout.
What problem does this solve?
Pingora currently exposes
PeerOptions::read_timeoutas a per-read inactivity timeout. That is useful for a silent upstream, but it does not bound the total time spent receiving an incomplete response header when the upstream keeps making small amounts of progress before each individual read timeout expires.At current
maincommit09696b51bc59315353d96686355861604d0bb48c, the HTTP/1 clientHttpSession::read_response()keeps partial bytes inresponse_header_read_buf, then wraps eachunderlying_stream.read_buf(...)in a freshread_timeout. Any successful read loops back to parsing and receives a new timeout. The public peer documentation and both H1/H2 client fields also describeread_timeoutas resetting on every read and explicitly not being an overall response-duration timeout.That means an upstream can send an incomplete response header one byte/fragment at a time, each fragment arriving inside
read_timeout, and keep a request occupied without ever completing\r\n\r\nor violating the inactivity timeout.This cannot be implemented reliably by a
ProxyHttpapplication today:upstream_response_filteris invoked only after the response header has arrived, while body filters are later still.total_connection_timeoutis not a substitute because it bounds connection establishment (including TLS), not the response-header phase.Desired behavior
Please consider an additive, explicit overall upstream response-header deadline (name/API shape up to maintainers) that is distinct from
read_timeout.The important semantics would be:
read_timeoutremains independently useful as an inactivity timeout;A peer option is one possible surface, but an equivalent cancellation-safe hook would also solve the application-level gap if it can actually terminate a pending header read at the overall deadline.
Reproduction shape
A deterministic regression can use an origin that:
Nms whereN < read_timeout;\r\n\r\npast the configured overall header deadline.Expected with a new overall deadline: the request fails at approximately that total header budget even though every individual read made progress inside
read_timeout.This is specifically about bounding the response-header phase. It is separate from whole-response/body lifetime policy and from application-level streaming semantics.