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]

Reply via email to