Skip to content

Restore response body streaming over HTTP/2.0 #1290

Description

@SSE4

Add a description

Response body streaming (HttpHandlerBase::HandleStreamRequest /
HttpResponse::SetStreamBody) works over HTTP/1.1 but is disabled over
HTTP/2.0
. A streaming handler that receives an HTTP/2 request currently trips
a hard UINVARIANT whose throw escapes into std::terminate, taking down the
entire process — not just the offending request.

The feature was fully implemented and tested in the past, then temporarily
disabled. Most of the machinery is still in the tree; the connection-loop
integration that drives it was dropped during a refactor and the tests were
skipped. This issue asks to restore it, and — independently and more urgently —
to make the disabled path fail gracefully instead of aborting the process.

Observed on 3.2-rc
(1838cdcec).
All line links below are permalinks pinned to that commit.

Reproduction

Any handler that overrides HandleStreamRequest (i.e. response-body-stream: true) and is served on an HTTP/2 listener (http-version: '2') aborts the
process the moment such a request is handled:

// core/src/server/http/http_response.cpp:465
UINVARIANT(false, "Streaming in HTTP/2.0 is not supported currently.");

http_response.cpp#L465

Because a single listener with http-version: '2' also accepts HTTP/1.1 and
h2c fallback, a service cannot simply "avoid" this: any h2-capable client
reaching a streaming handler crashes the whole service. Our current workaround
is to override IsStreamed() to return false for request.GetHttpMajor() == 2 and buffer the response instead — which is exactly the graceful degradation
we think the framework should do by default (see "Ask 1" below).

History (archeology)

  • d6f69258f
    feat core: add body streaming for HTTP/2.0 (Oct 2024). Added the feature
    together with functional tests
    (test_http2_streaming.py).
  • 01bf20de1
    — the streaming tests were skipped under ticket TAXICOMMON-10258 (Mar 2025).
  • f75379023
    separate HttpReader and ConnectionBase (Feb 2026). The connection refactor
    removed the streaming-event wiring from the HTTP/2 connection loop and left the
    UINVARIANT gate in place.

Per a userver maintainer, it was turned off because it broke after an
update
and because it blocked concurrent HTTP/2 — one streaming response
head-of-line-blocked the other multiplexed streams on the connection. So the
re-enable is gated on solving the concurrency problem, not just flipping the
switch back on.

Current state of the code

The producer/consumer machinery is still present and intact:

Piece Location
Gate that aborts http_response.cpp#L465
Stream event producer (wired on request construction) http2_session.cpp#L347
Streaming queue + event members http2_session.hpp#L132-L134
Consumer drain (HandleStreamingEvents) http2_session.cpp#L371
Event accessor (GetStreamingEvent) http2_session.cpp#L369
Chunk-split / NGHTTP2_ERR_DEFERRED read callback (GetMaxSize) http2_stream.cpp#L79-L93
Deferred provider submit on streaming http2_writer.cpp#L119

The missing link: Http2Session::HandleStreamingEvents()
(L371)
and Http2Session::GetStreamingEvent()
(L369)
are defined but have zero callers. The HTTP/2 connection loop —
Http2Connection::ListenForRequests()
— builds a
WaitAnyContext
that only waits on two things: the socket becoming readable
(kSocketReadable) and per-request handler-task completion
(kTaskComputedResponse). It never appends session.GetStreamingEvent() to the
wait_any, and never calls session.HandleStreamingEvents(). As a result, when
a streaming handler pushes a body chunk it signals streaming_event_ and enqueues
onto streaming_queue_
(producer wired at L347),
but nothing wakes to drain the queue and resume the deferred nghttp2 data
provider. The producer end is connected; the consumer end in the connection loop
is not.

So restoring streaming is roughly:

  1. Wait on session.GetStreamingEvent() in the
    WaitAnyContext loop,
    with a new WakeupKind (e.g. kStreamingReady) that calls
    session.HandleStreamingEvents() to resume deferred streams and flush pending
    frames.
  2. Remove the UINVARIANT gate at
    http_response.cpp#L465
    (the emplace of Http2StreamEventProducer on the line below it is already
    the correct branch).
  3. Solve the concurrency / head-of-line blocking that caused the original
    disable: a streaming response must not stall other multiplexed streams on the
    same connection. This is the real engineering work and the reason it was
    parked — draining/interleaving must be per-stream and cooperative, respecting
    flow-control windows, so a slow producer only defers its own stream.

Asks

Ask 1 (small, worth doing regardless of streaming): degrade gracefully instead
of std::terminate.
Even if full streaming stays disabled, hitting a
streaming handler over HTTP/2 should not crash the whole process. Options: buffer
the streamed body transparently, or fail just that one request (e.g. 500), rather
than
UINVARIANT(false, ...).
Today a single h2 request to a streaming handler is a process-wide DoS.

Ask 2 (the feature): re-enable HTTP/2 body streaming by restoring the
connection-loop integration and resolving the concurrency blocker, then
un-skipping the tests.

Verifying it works

There is an existing, ready-made harness — it just needs un-skipping and one
addition:

Recommended acceptance criteria:

  1. All three tests un-skipped and green.
  2. Extend test_body_stream_concurrent to assert progress interleaving — a
    slow/large streamed response on one stream must not block completion of other
    concurrent streams on the same connection (the exact failure that caused the
    disable). A backpressure/flow-control case (WINDOW_UPDATE gated producer)
    would make the guarantee explicit.
  3. No process-wide abort under any streaming-over-h2 scenario (covers Ask 1 too).

References

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