Skip to content

mod_http2: reset truncated HTTP/2 responses; spare header-only ones#676

Draft
mmontalbo wants to merge 2 commits into
apache:trunkfrom
mmontalbo:mm/h2-cgi-timeout-reset
Draft

mod_http2: reset truncated HTTP/2 responses; spare header-only ones#676
mmontalbo wants to merge 2 commits into
apache:trunkfrom
mmontalbo:mm/h2-cgi-timeout-reset

Conversation

@mmontalbo

@mmontalbo mmontalbo commented Jun 29, 2026

Copy link
Copy Markdown

This series addresses two ways mod_http2 mis-terminates an HTTP/2 stream when
a c2 (request) handler finishes without sending an EOS. Patch 1 completes the
PR 69580 fix on trunk; patch 2 fixes a separate hang (PR 70131) on the 2.4.x
build path. Both come from one gap: h2_beam_is_complete() cannot tell a
header-only response (204/304, and HEAD, complete without a body EOS) from a
truncated one, and mod_http2 compiles one of two c2 output paths depending on
the server's module magic (AP_HAS_RESPONSE_BUCKETS is 1 for MMN >= 20211221,
0 for 2.4.x), so the two paths fail in opposite directions: the
response-bucket path resets a valid 304, and the legacy path leaves a
truncated response hanging.

r1924267 fixed PR 69580 by exempting header-only responses from the
missing-EOS reset in stream_data_cb(), and that exemption was backported to
2.4.x (r1928581), where it is enough: the legacy c2_process() closes the
output beam, so the 304's beam is complete by the time s_c2_done() runs and
stream_data_cb() is the only reset path. On trunk the response-bucket path
leaves the beam incomplete, and s_c2_done()'s own abort (added in the 2023
HTTP/2 WebSockets work) still resets the 304, a path r1924267 did not cover
and which is not exercised on 2.4.x. PR 69580 therefore still reproduces on
trunk. Patch 1 gives s_c2_done() the same header-only exemption that
stream_data_cb() already has.

The exemption is carried by a new conn_ctx flag, header_only ("the final
response is header-only: 204/304, or HEAD"), set once from
AP_STATUS_IS_HEADER_ONLY() where the response is finalized. The completeness
test at each c2 reset point is then h2_beam_is_complete() OR header_only: the
beam reports whether a body EOS was seen, and the flag supplies the one case
the beam cannot know. Both patches read the same flag, so the two output paths
make the decision the same way.

With these patches:

  • A header-only response (204/304, and HEAD) is no longer RST_STREAM'd on
    trunk. A mod_cache revalidation 304 (EOR + Flush, no body EOS) now closes
    cleanly rather than resetting the stream (PR 69580).

  • A response that started but never sent EOS (e.g. a CGI that hits the
    server Timeout mid-body) is reset instead of left parked, on the legacy
    c2 path. Previously a client without a stall timeout waited indefinitely,
    while the same handler over HTTP/1.1 closed the connection and failed
    fast (PR 70131).

The completeness decision is layered at the c2 level rather than pushed into
h2_beam_is_complete(): the beam has no view of the response status, so it
cannot know that a body-less 304 is complete, whereas the c2 filters and
conn_ctx do. h2_beam_is_complete() therefore stays a pure beam-state query,
and patch 1's exemption sits next to the one stream_data_cb() already applies.

Patch 2 changes only the legacy (!AP_HAS_RESPONSE_BUCKETS) path, which is not
compiled on trunk, and trunk does not have the hang. The worker enters c2 by
build (h2_workers.c): trunk calls ap_process_connection() directly, while
2.4.x calls h2_c2_process(), then c2_process(). c2_process() is the module's
only h2_beam_close() call site, so trunk never force-closes the output beam,
and a truncated response leaves it incomplete for s_c2_done() to reset. Patch 2
is therefore inert on trunk and becomes active when the module is built for, or
synced to, 2.4.x. Patch 1's response-bucket fix is the live fix on trunk.

  • Patch 1: complete r1924267 (PR 69580) on trunk. Introduce
    conn_ctx->header_only, set from AP_STATUS_IS_HEADER_ONLY() where the
    response is finalized (h2_c2_filter_out on the response-bucket path,
    pass_response / h2_c2_filter_response_out on the legacy path). In
    s_c2_done(), spare a header-only response from the "incomplete output"
    abort, since its beam is legitimately EOS-less, so a body-less mod_cache
    revalidation 304 is no longer reset. Adds test_h2_105_21 (a 304
    revalidation must close cleanly).

  • Patch 2: reset an incomplete response on the legacy c2 path (PR 70131).
    There the worker runs c2_process(), which closes the output beam
    unconditionally when the handler returns, so a response that began but
    never sent EOS leaves a closed beam that h2_beam_is_complete() reports
    complete; s_c2_done() then sends no reset, and stream_data_cb()'s
    APR_EOF reset never fires: h2_beam_receive() returns APR_EOF only on a
    zero-transfer receive of the closed beam, but the single re-dispatch
    drains the trailing EOS-less bucket and returns APR_SUCCESS, so the
    stream re-suspends with no further wakeup, parking it. Abort the beam
    instead of closing it when a final response was
    started, its beam is incomplete, and the response is not header-only; an
    aborted beam returns APR_ECONNABORTED from h2_beam_receive() on the next
    pull (checked before the transfer loop), so the reset fires regardless of
    how the wakeup is scheduled. Header-only responses set header_only on this
    path in patch 1, so they are spared. Adds test_h2_105_20 (a CGI that goes
    silent past Timeout, run as concurrent streams, must be reset).

[1] PR 69580: https://bz.apache.org/bugzilla/show_bug.cgi?id=69580
[2] PR 70131: https://bz.apache.org/bugzilla/show_bug.cgi?id=70131
(Note: this series replaces the patch attached to the bug report)

@mmontalbo mmontalbo marked this pull request as draft June 29, 2026 18:12
@mmontalbo mmontalbo changed the title mod_http2: RST_STREAM an incomplete HTTP/2 response instead of hanging mod_http2: RST_STREAM incomplete HTTP/2 response to avoid hanging Jun 29, 2026
@mmontalbo mmontalbo changed the title mod_http2: RST_STREAM incomplete HTTP/2 response to avoid hanging mod_http2: RST_STREAM incomplete HTTP/2 response to avoid hang Jun 29, 2026
@mmontalbo mmontalbo changed the base branch from 2.4.x to trunk June 30, 2026 01:45
@mmontalbo mmontalbo changed the base branch from trunk to 2.4.x June 30, 2026 01:45
@mmontalbo mmontalbo force-pushed the mm/h2-cgi-timeout-reset branch from 11acde1 to 094f580 Compare June 30, 2026 01:47
@mmontalbo mmontalbo changed the base branch from 2.4.x to trunk June 30, 2026 01:47
@mmontalbo mmontalbo changed the title mod_http2: RST_STREAM incomplete HTTP/2 response to avoid hang mod_http2: reset truncated HTTP/2 responses while sparing header-only ones Jun 30, 2026
@mmontalbo mmontalbo changed the title mod_http2: reset truncated HTTP/2 responses while sparing header-only ones mod_http2: reset truncated HTTP/2 responses; spare header-only ones Jun 30, 2026
@mmontalbo mmontalbo force-pushed the mm/h2-cgi-timeout-reset branch 2 times, most recently from 0261108 to eaeb3fc Compare July 1, 2026 01:37
A 204/304 (and HEAD) response carries no body and ends without a body EOS, so
h2_beam_is_complete() reports the output beam as incomplete. s_c2_done() then
RST_STREAM'd such a stream as if it were truncated, re-opening PR 69580 for
body-less mod_cache 304 revalidations.

Track in conn_ctx whether the final response is header-only (set from
AP_STATUS_IS_HEADER_ONLY where the response passes out) and skip the
incomplete-output reset for it in s_c2_done(). This mirrors the c1 output
path, which already excludes header-only responses via
AP_STATUS_IS_HEADER_ONLY(). Only a response that began a body and never
finished it is reset.
On the legacy c2 path, c2_process() always h2_beam_close()d the output beam
when the handler returned. A response whose body never completed, e.g. a CGI
that sent status, headers and a partial body and then timed out, was closed
with no EOS. h2_beam_is_complete() then reported it complete, so no RST_STREAM
was sent and the client hung waiting for data that never came.

Abort the beam instead when a final response was started but never completed
and is not header-only, the same condition s_c2_done() uses. Header-only
responses (204/304/HEAD) are still closed normally; only genuinely truncated
responses are reset.
@mmontalbo mmontalbo force-pushed the mm/h2-cgi-timeout-reset branch from eaeb3fc to 29c2d2c Compare July 1, 2026 02:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant