bryancall opened a new issue, #13544:
URL: https://github.com/apache/trafficserver/issues/13544
## Summary
When an origin sends a `100 Continue` over HTTP/2 and ATS discards it below
the state
machine, the request body is never forwarded. Depending on whether the
origin waits for
the body, this either silently drops the body while returning `200 OK` to
the client, or
hangs the transaction.
This is the follow-up to the 1xx interim-response handling added in #13351.
That change
correctly stops an interim response from corrupting the final response
headers, but a
`100 Continue` is load-bearing for the request body and cannot simply be
dropped.
## Why the body is stranded
`HttpSM` deliberately withholds the request body until it observes the
origin's 100:
- `proxy.config.http.send_100_continue_response` defaults to `0`, so
`HttpSM.cc:812` sets `m_100_continue_required`.
- `HttpTransact.cc:8065` only strips `Expect:` when `m_100_continue_sent`,
which is false
at that default, so `Expect: 100-continue` is forwarded to the origin.
- `HttpSM.cc:2226` then skips `do_setup_client_request_body_tunnel()` while
`m_100_continue_required` is set.
- The only path that releases the body is `tunnel_handler_100_continue`
(`HttpSM.cc:3136`), reached via
`HttpTransact::handle_100_continue_response`. That
requires the state machine to actually see the 100.
## Observed
Client sends `POST` with `Expect: 100-continue` and a 14 byte body; origin
speaks
HTTP/2. From `traffic.out` on the outbound session:
```
Send HEADERS frame flags: 0x4 length: 178 <- END_HEADERS only, no
END_STREAM
change_state ... IDLE -> OPEN <- body withheld
Received HEADERS frame
received interim 1xx response from origin; awaiting final response
Received HEADERS frame <- final 200
Received DATA frame, flags: 1
change_state ... OPEN -> HALF_CLOSED_REMOTE
```
ATS advertised `Content-Length: 14` to the origin and never sent a single
DATA frame.
The stream never reaches `HALF_CLOSED_LOCAL`.
Two failure modes, depending on the origin:
| Origin behavior | Result |
| --- | --- |
| Responds without waiting for the body | Body silently dropped; client
receives `200 OK`. No error logged. |
| Waits for the body after its 100, as an origin honoring `Expect` should |
Transaction hangs. `curl` reports `Operation timed out after 15002 milliseconds
with 0 bytes received`; ATS eventually fails it at
`transaction_no_activity_timeout_out` (30s default). |
The first case is the more serious one: a `POST` reported to the client as
successful
whose payload never reached the origin.
Note this diverges from the HTTP/1.x origin path, which routes the 1xx
through
`handle_100_continue_response` and forwards it. Same client, same origin
semantics,
different outcome based only on the origin's protocol.
## Two fixes that do not work
Both of these were tried and made things worse, so the fix needs more than a
carve-out:
1. **Excluding `100` from the discard.** The client receives the `100`, then
`502 Malformed Server Response Status`, with `recv headers malformed
request` logged.
Nothing resets the receive header between decodes, so the final response
decodes a
second `:status` into the same header. This is the exact corruption
#13351 fixes.
2. **Calling `send_headers()` to propagate the interim, then resetting.**
Worse; nearly
every interim case regresses.
So the outbound HTTP/2 path cannot currently deliver an interim response to
the state
machine at all. Making 1xx work end to end (which would also let `103 Early
Hints` reach
the client per RFC 9110 15.2, as the HTTP/1 origin path already does) looks
like the real
scope here.
## Reproducer
Adds an `expectwait` mode to the interim-response gold test origin: send the
`100`, then
wait for a request-body DATA frame before the final `200`.
```diff
--- a/tests/gold_tests/h2/h2_interim_origin.py
+++ b/tests/gold_tests/h2/h2_interim_origin.py
@@ def handle(sock: ssl.SSLSocket, mode: str) -> None:
if ftype == 0x1: # a request HEADERS -> respond on the
same stream
+ if mode == "expectwait":
+ # Honor Expect: 100-continue the way a correct
origin does: send
+ # the 100 and then wait for the body before the
final response.
+ sock.sendall(frame(0x1, 0x4, sid,
interim_block("100")))
+ continue
send_response(sock, mode, sid)
+ if ftype == 0x0 and mode == "expectwait": # request body
arrived
+ sock.sendall(frame(0x1, 0x4, sid, final_block()))
+ sock.sendall(frame(0x0, 0x1, sid, BODY))
```
Add `expectwait` to the `--mode` choices and to the test's mode list, then:
```
curl -v -s --max-time 15 -X POST --data-binary "payloadpayload" \
-H "Expect: 100-continue" -H "Host: ats.test" \
http://127.0.0.1:<ts_port>/expectwait
```
Expected: `200` with the response body. Actual: client timeout, no bytes
received.
Pointing the same request at the existing `continue` mode origin instead
shows the
silent-body-drop variant: `200 OK` to the client, no DATA frame to the
origin.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]