Skip to content

fix: trigger initial body send when downstream already consumed#1

Open
nerdalert wants to merge 1 commit intopraxis-proxy:certificate-publicfrom
nerdalert:brent-streambuffer-replay-limit
Open

fix: trigger initial body send when downstream already consumed#1
nerdalert wants to merge 1 commit intopraxis-proxy:certificate-publicfrom
nerdalert:brent-streambuffer-replay-limit

Conversation

@nerdalert
Copy link
Copy Markdown
Member

@nerdalert nerdalert commented May 7, 2026

Details and options evaluated are in praxis-proxy/praxis#179

When a ProxyHttp implementation pre-reads the request body for inspection before upstream selection, downstream is already done when duplex mode begins. The initial body-send path was only entered for retry buffers or empty bodies, skipping the case where request_body_filter needs a terminal callback to emit the pre-read body.

Add body_already_consumed check across h1, h2, and custom transports so pre-read body replay works without relying on the 64 KiB retry buffer workaround."

Validation

Sends a ~70 KB JSON-RPC body ("x".repeat(70_000) inside a JSON wrapper) through a json_rpc filter with max_body_bytes: 131072 (128 KiB). The json_rpc filter uses StreamBuffer mode, which pre-reads the body before routing. An echo backend sends the received body back. The test asserts the echoed body length matches the original.

Before the Pingora fix: the 64 KiB retry buffer truncated the body, so the backend received ~65 KB instead of ~70 KB. The assertion failed on the length mismatch.

The test is in a yet to be pushed agentic PR that looks like this. It now passes using that branch along with passing all of the praxis IT suite:

 #[test]
  fn stream_buffer_body_above_64kib_forwarded_intact() {
      let backend_guard = start_echo_backend_with_shutdown();
      let proxy_port = free_port();

      let yaml = format!(
          r#"
  listeners:
    - name: default
      address: "127.0.0.1:{proxy_port}"
      filter_chains: [main]
  filter_chains:
    - name: main
      filters:
        - filter: json_rpc
          max_body_bytes: 131072
        - filter: router
          routes:
            - path_prefix: "/"
              cluster: "backend"
        - filter: load_balancer
          clusters:
            - name: "backend"
              endpoints:
                - "127.0.0.1:{}"
  "#,
          backend_guard.port()
      );
      let config = Config::from_yaml(&yaml).unwrap();
      let proxy = start_proxy(&config);

      let payload = format!(
          r#"{{"jsonrpc":"2.0","id":1,"method":"test","params":{{"data":"{}"}}}}"#,
          "x".repeat(70_000)
      );
      let request = format!(
          "POST / HTTP/1.1\r\n\
           Host: localhost\r\n\
           Content-Type: application/json\r\n\
           Content-Length: {}\r\n\
           \r\n\
           {payload}",
          payload.len(),
      );

      let raw = http_send(proxy.addr(), &request);
      assert_eq!(
          echoed.len(),
          payload.len(),
          "backend should receive the full body ({} bytes), but got {} bytes — \
           Pingora retry buffer truncates at 64 KiB (BODY_BUF_LIMIT)",
          payload.len(),
          echoed.len()
      );
  }

Copy link
Copy Markdown

Copilot AI left a comment

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 request body forwarding for the case where a ProxyHttp implementation fully consumes (pre-reads) a non-empty downstream request body before the proxy enters duplex streaming to the upstream. It ensures the “initial body send” path is triggered so request_body_filter() receives a terminal callback and can replay its buffered body, without relying on the 64 KiB retry buffer.

Changes:

  • Add a body_already_consumed condition (downstream already done + body not empty) to enter the initial body-send path for H1, H2, and custom upstream transports.
  • Refactor retry-buffer handling to pass an Option<Bytes> directly (instead of destructuring and re-wrapping) where applicable.
  • Keep existing empty-body handling semantics (H1 uses the initial path; H2/custom continue to handle empty bodies at header send time).

Reviewed changes

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

File Description
pingora-proxy/src/proxy_h2.rs Ensures H2 upstream body sending enters the initial send path when downstream body was pre-consumed, enabling terminal request_body_filter() replay.
pingora-proxy/src/proxy_h1.rs Extends H1 initial send conditions to cover pre-consumed (non-empty) downstream bodies so filters can emit buffered bodies.
pingora-proxy/src/proxy_custom.rs Applies the same pre-consumed body handling to custom upstream transports to avoid missing the filter terminal callback.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +386 to +397
let buffer = session.as_mut().get_retry_buffer();
let body_empty = session.as_mut().is_body_empty();

// Enter the initial body-send path when:
// - a retry buffer exists (normal retry replay), or
// - the downstream body was already consumed before forwarding
// (e.g. a ProxyHttp implementation that pre-reads the body for
// inspection). In that case request_body_filter() needs one
// terminal callback so the implementation can emit its
// buffered body to upstream.
let body_already_consumed = downstream_state.is_done() && !body_empty;
if buffer.is_some() || body_already_consumed {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Added a targeted regression test for Copilot’s comment:

  • Fully consumes the downstream request body in request_filter().
  • Stores the pre-consumed body in the test context.
  • Emits that buffered body from request_body_filter() on the terminal callback.
  • Sends the request over H2C.
  • Forces H2 upstream with x-h2: true.
  • Uses a payload larger than BODY_BUF_LIMIT: 128 KiB + 17.
  • Asserts the upstream /echo response body exactly matches the full payload.

@nerdalert nerdalert force-pushed the brent-streambuffer-replay-limit branch from b8ee4a0 to 85eab8c Compare May 9, 2026 00:10
  When a ProxyHttp implementation pre-reads the request body for
  inspection before upstream selection, downstream is already done
  when duplex mode begins. The initial body-send path was only
  entered for retry buffers or empty bodies, skipping the case
  where request_body_filter needs a terminal callback to emit
  the pre-read body.

  Add body_already_consumed check across h1, h2, and custom
  transports so pre-read body replay works without relying on
  the 64 KiB retry buffer workaround."

Signed-off-by: Brent Salisbury <bsalisbu@redhat.com>
@nerdalert nerdalert force-pushed the brent-streambuffer-replay-limit branch from 85eab8c to 1c8d088 Compare May 9, 2026 03:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Review

Development

Successfully merging this pull request may close these issues.

3 participants