Skip to content

fix(openai_responses): centralize body-size enforcement - #700

Open
eoinfennessy wants to merge 3 commits into
praxis-proxy:mainfrom
eoinfennessy:fix/centralize-responses-body-size-enforcement
Open

fix(openai_responses): centralize body-size enforcement#700
eoinfennessy wants to merge 3 commits into
praxis-proxy:mainfrom
eoinfennessy:fix/centralize-responses-body-size-enforcement

Conversation

@eoinfennessy

Copy link
Copy Markdown
Member

Motivation

Several OpenAI Responses filters carried a max_body_bytes knob that only sized their StreamBuffer buffer. Because praxis core merges per-direction body modes across a chain (keeping the largest) before clamping to the listener's transport ceiling, that knob was silently widened whenever a sibling filter declared a larger buffer — so it never reliably capped the raw body it appeared to control.

Closes #562.

Approach

Raw transport size is now governed exclusively by the pipeline's body_limits (max_request_bytes / max_response_bytes), enforced once by core before any filter runs. Per-filter limits are reserved for the body a filter genuinely produces, which body_limits cannot see.

  • Shared helpers (apis/src/openai/responses/body_limits.rs): validate_size_limit (names the actual field in errors) and reject_rewritten_body_too_large (streaming-aware 413).
  • Producer filters (doc_extract, file_resolve, openai_mcp_tool_resolve, openai_responses_proxy) enforce max_rewritten_body_bytes / max_resolved_bytes on the rewritten body only; their buffers accept up to the 64 MiB absolute ceiling.
  • Pure-BodyMode filters (openai_responses_format, openai_responses_model_rewrite, agentic_loop, openai_mcp_dispatch, openai_tool_parse) drop the knob entirely and rely on body_limits.
  • responses_to_chat_completions renames max_body_bytesmax_rewritten_body_bytes to reflect that it bounds the translated body, not the raw input.

Breaking change

Removed fields are now rejected under #[serde(deny_unknown_fields)]. A config that still sets max_body_bytes on a migrated filter fails loudly at startup rather than silently ignoring the value. Example configs and filter docs are updated to the new form.

Testing

  • New body-size-limits example config plus a functional integration test asserting the transport cap rejects oversized raw bodies with 413 and forwards small ones.
  • Unit/integration coverage updated across all migrated filters (legacy-knob-rejected assertions added).
  • make test and make lint pass; filter docs, example README, and coverage manifests regenerated and in sync.

Several OpenAI Responses filters carried a `max_body_bytes` knob that only
sized their `StreamBuffer` buffer. Because praxis core merges per-direction
body modes across a chain (keeping the largest) before clamping to the
listener's transport ceiling, that knob was silently widened whenever a
sibling filter declared a larger buffer — so it never reliably capped the
raw body it appeared to control (issue praxis-proxy#562).

Raw transport size is now governed exclusively by the pipeline's
`body_limits` (`max_request_bytes` / `max_response_bytes`), enforced once by
core before any filter runs. Per-filter limits are reserved for the body a
filter genuinely *produces*, which `body_limits` cannot see.

- Add shared `body_limits` helpers: `validate_size_limit` (names the actual
  field) and `reject_rewritten_body_too_large` (streaming-aware 413).
- Producer filters (doc_extract, file_resolve, openai_mcp_tool_resolve,
  openai_responses_proxy) enforce `max_rewritten_body_bytes` /
  `max_resolved_bytes` on the rewritten body only; buffers accept up to the
  64 MiB absolute ceiling.
- Pure-BodyMode filters (openai_responses_format,
  openai_responses_model_rewrite, agentic_loop, openai_mcp_dispatch,
  openai_tool_parse) drop the knob entirely and rely on `body_limits`.
- responses_to_chat_completions renames `max_body_bytes` ->
  `max_rewritten_body_bytes` to reflect that it bounds the translated body.

Removed fields are now rejected under `deny_unknown_fields` (a breaking
config change surfaced loudly at startup rather than silently ignored).

Add the body-size-limits example config and its functional integration
test; update example configs, filter docs, and unit/integration coverage.

Signed-off-by: Eoin Fennessy <efenness@redhat.com>

rh-pre-commit.version: 2.4.0
rh-pre-commit.check-secrets: ENABLED
@eoinfennessy
eoinfennessy requested review from a team and jland-redhat August 10, 2026 22:03

@praxis-bot praxis-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed all 47 changed files across the centralized body-size enforcement migration.

Approach: Sound. Raw transport size enforcement moves to body_limits (pipeline-level), while producer filters retain a renamed max_rewritten_body_bytes / max_resolved_bytes for the body they produce. This correctly separates transport caps from semantic caps.

Checked:

  • body_limits.rs: validate_size_limit correctly rejects zero and above-ceiling values; reject_rewritten_body_too_large delegates to the streaming-aware responses_error_rejection envelope. Tests cover zero, above-ceiling, at-ceiling, non-streaming JSON, and streaming SSE.
  • All pure-BodyMode filters (responses_format, model_rewrite, agentic_loop, mcp_dispatch, tool_parse) correctly drop max_body_bytes and declare StreamBuffer { max_bytes: Some(MAX_JSON_BODY_BYTES) }. Legacy max_body_bytes is rejected at parse time via deny_unknown_fields (verified tests exist for each).
  • All producer filters (doc_extract, file_resolve, mcp_tool_resolve, responses_proxy, responses_to_chat_completions) correctly rename to max_rewritten_body_bytes / max_resolved_bytes, validate through the centralized helper, and enforce only on the produced body.
  • Pipeline test (resolve_pipelines_transport_limit_governs_openai_responses_raw_body) proves the transport cap clamps the merged 64 MiB StreamBuffer declarations.
  • Integration test verifies both under-cap forwarding (200) and over-cap rejection (413).
  • Streaming detection is consistent across all filters (metadata from openai_responses_format.stream).
  • Error types and status codes are consistent: 413 / invalid_request_error for oversized rewritten bodies.
  • Example config, docs, and README updates are in sync.

No issues found at critical, large, or medium severity.

@praxis-bot praxis-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed the merge commit (c332136) that integrates nine main-branch changes into this PR.

Merge brought in two unrelated features from main:

  1. mcp_dispatch: normalize_arguments / extract_arguments refactor (argument parsing, double-encoding fix). No interaction with body-size enforcement.
  2. responses/mod.rs: authoritative POST /v1/responses classification (discriminator-less create bodies classify as openai_responses instead of unknown_json). New classify_request logic, tests, example config docs, integration tests.

Checked:

  • mcp_dispatch/mod.rs merge: normalize_arguments and the body-size changes (MAX_JSON_BODY_BYTES ceiling, max_body_bytes field removal) coexist cleanly. No conflict artifacts.
  • responses/mod.rs merge: body::MAX_JSON_BODY_BYTES import and request_body_mode ceiling change merged correctly alongside the new is_responses_create import and classify_request rewrite.
  • responses/tests.rs: run_filter_raw now delegates to run_filter_raw_with_method defaulting to POST /v1/responses. The PR's legacy_max_body_bytes_rejected and body_mode_is_stream_buffer tests are unaffected because they exercise config parsing and body mode, not classification.
  • format-routing.yaml, openai_responses_format.md, integration test updates from main are documentation/config changes with no body-size overlap.
  • Infrastructure changes (Cargo.lock, deny.toml, CI workflow) are orthogonal.

Note: mergeable_state: dirty -- main is 9 commits ahead since this merge. The branch will need another merge or rebase before it can land.

No new issues found in the merge commit.

@praxis-bot praxis-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed all 48 changed files across the centralized body-size enforcement migration.

Approach: Sound. Separating raw transport caps (body_limits) from per-filter produced-body caps (max_rewritten_body_bytes / max_resolved_bytes) is the correct architecture. The old max_body_bytes knob was misleading because core merged per-direction body modes across the chain before clamping to the transport ceiling, making per-filter raw caps unreliable.

Checked:

  • body_limits.rs helpers: validate_size_limit correctly rejects zero and above-ceiling values; reject_rewritten_body_too_large builds streaming-aware 413s via the shared responses_error_rejection envelope. Unit tests cover all branches.
  • All pure-BodyMode filters (responses_format, model_rewrite, agentic_loop, mcp_dispatch, tool_parse) correctly drop max_body_bytes and declare StreamBuffer { max_bytes: Some(MAX_JSON_BODY_BYTES) }. Legacy field rejected via deny_unknown_fields with test coverage for each.
  • All producer filters (doc_extract, file_resolve, mcp_tool_resolve, responses_proxy, responses_to_chat_completions) correctly rename to max_rewritten_body_bytes / max_resolved_bytes, validate through validate_size_limit, and enforce only on the produced body.
  • file_resolve: new max_resolved_bytes properly decouples the per-file content limit from the total rewritten body limit. Previously both were overloaded onto max_body_bytes. Tests verify independence (max_resolved_bytes_bounds_individual_content_independent_of_rewritten_limit).
  • mcp_tool_resolve: new ResolveError::RewrittenTooLarge variant added with 413 mapping in resolve_error_rejection. Previously this filter had no rewritten body size check at all -- this is a genuine improvement.
  • Raw body checks removed from doc_extract and file_resolve (reject_raw_body_too_large) -- correctly delegated to the pipeline transport cap.
  • Pipeline test (resolve_pipelines_transport_limit_governs_openai_responses_raw_body) proves the 64 MiB StreamBuffer declarations are clamped to body_limits.max_request_bytes.
  • Integration test covers both under-cap forwarding (200) and over-cap rejection (413).
  • Streaming detection is consistent across all producer filters (openai_responses_format.stream metadata).
  • Error types and status codes are consistent: 413 / invalid_request_error for oversized rewritten bodies.
  • Example config, filter docs, and README updates are in sync.

One medium observation noted inline.


serialized.commit(body, filter_name, "tools");
if serialized.len() > max_rewritten_body_bytes {
debug!(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

medium / consistency: This new oversized-body rejection path uses debug!, while doc_extract and file_resolve use warn! for the identical semantic event (rewritten body exceeds configured limit). At default log levels operators would see warnings for doc_extract/file_resolve rejections but miss mcp_tool_resolve rejections.

Note: responses_proxy and responses_to_chat_completions also use debug! for their equivalent paths, so the inconsistency is partially pre-existing. But since this PR centralizes body-size enforcement, it is an opportunity to align all producer filters on one level.

Suggestion:

warn!(
    actual = serialized.len(),
    limit = max_rewritten_body_bytes,
    "rewritten request body exceeds configured limit"
);

This would also align the tracing field names (actual/limit vs body_bytes/max_bytes) with the convention used in the other two producer filters.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Clarify and centralize body-size enforcement across OpenAI Responses filter chains

3 participants