mod_http2: reset truncated HTTP/2 responses; spare header-only ones#676
Draft
mmontalbo wants to merge 2 commits into
Draft
mod_http2: reset truncated HTTP/2 responses; spare header-only ones#676mmontalbo wants to merge 2 commits into
mmontalbo wants to merge 2 commits into
Conversation
11acde1 to
094f580
Compare
0261108 to
eaeb3fc
Compare
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.
eaeb3fc to
29c2d2c
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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)