Skip to content

Fix H2 origin payload handling - #13363

Merged
bneradt merged 1 commit into
apache:masterfrom
bneradt:h2-origin-payload-handling
Jul 29, 2026
Merged

Fix H2 origin payload handling#13363
bneradt merged 1 commit into
apache:masterfrom
bneradt:h2-origin-payload-handling

Conversation

@bneradt

@bneradt bneradt commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

This breaks out HTTP/2 work from this otherwise HTTP/3 focused PR:
#13213


HTTP/2 origin responses can legally carry a non-zero Content-Length
when no payload is sent, such as responses to HEAD requests. ATS
discarded the outbound request method after encoding the H2 HEADERS
frame and could therefore reject a valid no-body response as a
payload-length error.

An H2 DATA sender also treated every byte visible through its
IOBufferReader as eligible for the current write. Reader availability is
independent of the finite VIO operation: VIO::ntodo() is the
authoritative boundary, and the ordinary network VConnection already
caps writes to it. Without that cap, the regression sent 327,675 bytes
for a 300,000-byte PUT and the H2 origin returned GOAWAY with
PROTOCOL_ERROR.

This retains the outbound request method on the H2 stream for response
validation. It also caps DATA payloads to the remaining write VIO bytes,
still setting END_STREAM when the final authorized bytes are sent, and
extends the H2 origin replay coverage with HEAD and large PUT cases.

Copilot AI review requested due to automatic review settings July 6, 2026 16:19
@bneradt bneradt added this to the 11.0.0 milestone Jul 6, 2026
@bneradt bneradt self-assigned this Jul 6, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes HTTP/2 origin-side edge cases in payload handling by preserving the outbound request method for response validation (e.g., HEAD with non-zero Content-Length but no body) and by capping outbound DATA frame payloads to the remaining write VIO byte count. It also extends the existing H2 origin replay gold tests to cover these regressions.

Changes:

  • Track the sent outbound request method on an H2 stream and use it when deciding whether Content-Length vs. received payload-length mismatches are valid for no-body responses.
  • Limit outbound H2 DATA frame payload length to the remaining write VIO bytes while still properly emitting END_STREAM when the transaction body is complete.
  • Extend H2 origin replay coverage (HEAD no-body w/ non-zero Content-Length, large PUT + response) and update expected metric outputs.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/proxy/http2/Http2ConnectionState.cc Caps DATA frame payload by remaining write VIO bytes; records the outbound request method when sending request headers.
include/proxy/http2/Http2Stream.h Adds write_vio_ntodo() accessor; uses stored sent request method for payload-length validation logic.
tests/gold_tests/h2/replay_h2origin/h2-origin.yaml Adds new replay scenarios for HEAD (non-zero Content-Length, no body) and large PUT request/response.
tests/gold_tests/h2/h2origin.test.py Updates squid log expectations for the new UUID ranges.
tests/gold_tests/h2/gold/http-request-method-metrics.gold Updates the expected PUT request metric count to reflect the new PUT test.
Comments suppressed due to low confidence (1)

include/proxy/http2/Http2Stream.h:429

  • Typo in the Warning message key: "data_legnth" should be "data_length" to make the log output searchable/consistent.
  if (content_length != 0 && !is_payload_precluded && content_length != data_length) {
    Warning("Bad payload length content_length=%d data_legnth=%d session_id=%" PRId64, content_length,
            static_cast<int>(data_length), _proxy_ssn->connection_id());

Comment thread tests/gold_tests/h2/h2origin.test.py
@bneradt
bneradt force-pushed the h2-origin-payload-handling branch 2 times, most recently from c99484e to 4d912fa Compare July 6, 2026 17:34
@bryancall
bryancall requested a review from maskit July 6, 2026 22:03
@bneradt
bneradt requested review from masaori335 and removed request for maskit July 20, 2026 22:23

@maskit maskit left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when a buffer reader has more bytes available than should be sent on the stream

I wonder how that happens. Isn't that the root cause that needs to be fixed? The change on this PR look fine as a bandaid fix though. I'm not going to push this back, but want to confirm if I understand the issue correctly.

(_send_header.method_get_wksidx() == HTTP_WKSIDX_GET && _send_header.presence(mask) &&
_receive_header.status_get() == HTTPStatus::NOT_MODIFIED));
this->is_outbound_connection() &&
(_sent_request_method == HTTP_WKSIDX_HEAD || (_sent_request_method == HTTP_WKSIDX_GET && _send_header.presence(mask) &&

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note for future analysis: This could use _sm->t_state.hdr_info.server_request instead, but I'm not sure if we want to access it from here.

HTTP/2 origin responses can legally carry a non-zero Content-Length
when no payload is sent, such as responses to HEAD requests. ATS
discarded the outbound request method after encoding the H2 HEADERS
frame and could therefore reject a valid no-body response as a
payload-length error.

An H2 DATA sender also treated every byte visible through its
IOBufferReader as eligible for the current write. Reader availability is
independent of the finite VIO operation: VIO::ntodo() is the
authoritative boundary, and the ordinary network VConnection already
caps writes to it. Without that cap, the regression sent 327,675 bytes
for a 300,000-byte PUT and the H2 origin returned GOAWAY with
PROTOCOL_ERROR.

This retains the outbound request method on the H2 stream for response
validation. It also caps DATA payloads to the remaining write VIO bytes,
still setting END_STREAM when the final authorized bytes are sent, and
extends the H2 origin replay coverage with HEAD and large PUT cases.
@bneradt
bneradt force-pushed the h2-origin-payload-handling branch from 4d912fa to 70adfea Compare July 29, 2026 20:57
@bneradt

bneradt commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

when a buffer reader has more bytes available than should be sent on the stream

I wonder how that happens. Isn't that the root cause that needs to be fixed? The change on this PR look fine as a bandaid fix though. I'm not going to push this back, but want to confirm if I understand the issue correctly.

Thanks for asking. The original wording made this sound like an unexpected buffer state, so I updated the commit message and PR description to clarify it.
IOBufferReader::read_avail() is not the boundary of a write operation. do_io_write() receives both a reader and a byte count, and VIO::ntodo() is the authoritative number of bytes the VConnection may consume. The reader can legitimately expose more buffered data than remains in that finite VIO. The ordinary network VConnection already caps writes this way.
The H2 DATA sender was missing that contract enforcement. In the regression case, ATS sent 327,675 bytes for a 300,000-byte PUT, causing the H2 origin to return GOAWAY with PROTOCOL_ERROR. Therefore, limiting the DATA payload by ntodo() is the root fix rather than a band-aid.

@maskit maskit left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the clarification. The change makes sense now.

@bneradt
bneradt merged commit dcb1850 into apache:master Jul 29, 2026
15 checks passed
@bneradt
bneradt deleted the h2-origin-payload-handling branch July 29, 2026 22:09
@github-project-automation github-project-automation Bot moved this to For v10.2.0 in ATS v10.2.x Jul 29, 2026
bneradt added a commit to bneradt/trafficserver that referenced this pull request Jul 30, 2026
HTTP/2 origin responses can legally carry a non-zero Content-Length
when no payload is sent, such as responses to HEAD requests. ATS
discarded the outbound request method after encoding the H2 HEADERS
frame and could therefore reject a valid no-body response as a
payload-length error.

An H2 DATA sender also treated every byte visible through its
IOBufferReader as eligible for the current write. Reader availability is
independent of the finite VIO operation: VIO::ntodo() is the
authoritative boundary, and the ordinary network VConnection already
caps writes to it. Without that cap, the regression sent 327,675 bytes
for a 300,000-byte PUT and the H2 origin returned GOAWAY with
PROTOCOL_ERROR.

This retains the outbound request method on the H2 stream for response
validation. It also caps DATA payloads to the remaining write VIO bytes,
still setting END_STREAM when the final authorized bytes are sent, and
extends the H2 origin replay coverage with HEAD and large PUT cases.
@bneradt

bneradt commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

The following PR contains the 10.2.x version of this:
#13463

@bneradt bneradt removed this from ATS v10.2.x Jul 30, 2026
cmcfarlen pushed a commit that referenced this pull request Jul 31, 2026
HTTP/2 origin responses can legally carry a non-zero Content-Length
when no payload is sent, such as responses to HEAD requests. ATS
discarded the outbound request method after encoding the H2 HEADERS
frame and could therefore reject a valid no-body response as a
payload-length error.

An H2 DATA sender also treated every byte visible through its
IOBufferReader as eligible for the current write. Reader availability is
independent of the finite VIO operation: VIO::ntodo() is the
authoritative boundary, and the ordinary network VConnection already
caps writes to it. Without that cap, the regression sent 327,675 bytes
for a 300,000-byte PUT and the H2 origin returned GOAWAY with
PROTOCOL_ERROR.

This retains the outbound request method on the H2 stream for response
validation. It also caps DATA payloads to the remaining write VIO bytes,
still setting END_STREAM when the final authorized bytes are sent, and
extends the H2 origin replay coverage with HEAD and large PUT cases.

(cherry picked from commit dcb1850)
@cmcfarlen cmcfarlen removed this from the 11.0.0 milestone Jul 31, 2026
@cmcfarlen cmcfarlen added this to the 10.2.0 milestone Jul 31, 2026
@cmcfarlen

Copy link
Copy Markdown
Contributor

Cherry-picked to the 10.2.x branch as 7b31a6c for the 10.2.0 release.

bneradt added a commit to bneradt/trafficserver that referenced this pull request Jul 31, 2026
HTTP/2 origin responses can legally carry a non-zero Content-Length
when no payload is sent, such as responses to HEAD requests. ATS
discarded the outbound request method after encoding the H2 HEADERS
frame and could therefore reject a valid no-body response as a
payload-length error.

An H2 DATA sender also treated every byte visible through its
IOBufferReader as eligible for the current write. Reader availability is
independent of the finite VIO operation: VIO::ntodo() is the
authoritative boundary, and the ordinary network VConnection already
caps writes to it. Without that cap, the regression sent 327,675 bytes
for a 300,000-byte PUT and the H2 origin returned GOAWAY with
PROTOCOL_ERROR.

This retains the outbound request method on the H2 stream for response
validation. It also caps DATA payloads to the remaining write VIO bytes,
still setting END_STREAM when the final authorized bytes are sent, and
extends the H2 origin replay coverage with HEAD and large PUT cases.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants