Skip to content

feat(config): request_body_limit_bytes defaults to 0 = no cap; uniform 413 surface - #853

Merged
jarvis9443 merged 2 commits into
mainfrom
feat/body-limit-unlimited-default
Jul 30, 2026
Merged

feat(config): request_body_limit_bytes defaults to 0 = no cap; uniform 413 surface#853
jarvis9443 merged 2 commits into
mainfrom
feat/body-limit-unlimited-default

Conversation

@jarvis9443

@jarvis9443 jarvis9443 commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Problem

proxy.request_body_limit_bytes defaulted to 10 MiB. The reference LLM proxy ships with no request-body cap at all (its request-size guard defaults to off), and providers accept larger requests than any fixed gateway default — Anthropic's Messages API takes requests up to 32 MB. A 10 MiB gateway default therefore rejects requests the upstream would have served (base64 PDFs and images hit this in practice), and a client that works against the provider directly breaks the moment it goes through the gateway.

Two adjacent defects in the same family, found while aligning:

  1. 0 was accepted but meant "reject every request with a body" — DefaultBodyLimit::max(0) plus a declared > 0 check — instead of "no cap". There was no way to express "unlimited" at all (omitting the layer entirely would still leave axum's built-in 2 MiB extractor default).
  2. The 413 surface was inconsistent on the chunked/no-Content-Length path: five bare Json<Value> handlers (completions, images, rerank, responses, audio/speech) and the two raw-Bytes handlers (batches, fine-tuning) leaked axum's stock text/plain rejection instead of the OpenAI envelope; the multipart handlers (audio transcriptions/translations, files upload) folded axum's correctly-classified 413 into a generic 400; /mcp and /a2a returned a bare 400 for an over-cap body.

Change

  • Default request_body_limit_bytes0 = no cap, matching the reference proxy's out-of-box behaviour. 0 now installs DefaultBodyLimit::disable() (not merely a skipped layer — axum would otherwise fall back to its built-in 2 MiB), skips the Content-Length pre-check, and widens the manual to_bytes reads (/mcp, /a2a, /passthrough) accordingly. (The passthrough site was missed in the first cut — every passthrough POST got a 413 on the new default; caught by the independent audit, fixed with regression tests.) The duplicate-Content-Length rejection (request-smuggling hygiene) keeps firing regardless of the cap setting.
  • Configured limits behave exactly as before — both enforcement layers, the drain-before-413, and the per-path envelopes are unchanged — and the 413 surface is now uniform across every inbound-body route: JSON handlers map extractor-layer 413s through proxy_error_from_json_rejection, Bytes handlers through a new proxy_error_from_bytes_rejection, multipart handlers preserve axum's 413 classification via proxy_error_from_multipart, and /mcp + /a2a discriminate the length-limit error into the standard 413 envelope.

Behaviour changes

  1. Deployments without an explicit request_body_limit_bytes in their config no longer cap request bodies (previously 10 MiB). The shipped config.example.yaml / config.managed.yaml previously carried the value explicitly, so file-based deployments that started from them keep whatever their file says; the compiled default only applies when the key is absent. Operators who want a cap set the value — everything about configured behaviour is unchanged.
  2. With a configured cap, chunked over-limit requests on the seven previously-bare handlers now get the enveloped 413 (previously axum's text/plain), multipart over-limit uploads get 413 (previously 400), and /mcp//a2a over-limit bodies get the enveloped 413 (previously a bare 400). Malformed JSON on the five converted handlers now also gets the enveloped 400 instead of axum's stock rejection.

Tests

  • Unit: zero_limit_admits_bodies_over_axums_builtin_cap (a 2.5 MiB body — over axum's built-in 2 MiB — reaches the handler on both the declared-length and chunked paths), zero_limit_still_rejects_duplicate_content_length, chunked_oversize_on_v1_completions_returns_openai_envelope, chunked_oversize_on_v1_batches_returns_openai_envelope, chunked_oversize_multipart_returns_413_envelope. The three envelope/multipart tests fail against the previous handlers (verified).
  • Chunked-oversize coverage now also exists for /passthrough, /mcp and /a2a (the length-limit discrimination path), and /passthrough no longer mislabels transport faults as RequestTooLarge.
  • E2E: new body edges: unlimited default suite spawns the gateway with request_body_limit_bytes: 0 and drives a 12 MiB request through to the mock upstream; the existing 413 suite keeps its 10 MiB subject via a new dedicated harness override (the harness pins the old value so every existing with-cap assertion still tests what it always tested).

Summary by CodeRabbit

  • New Features

    • Request-body size limits can now be disabled by setting the limit to 0.
    • Requests exceeding configured limits consistently return HTTP 413 responses using the standard API error format.
  • Bug Fixes

    • Improved handling of oversized, malformed, chunked, JSON, and multipart requests across supported endpoints.
    • Non-size-related request-reading failures now return more accurate bad-request errors.
  • Tests

    • Added coverage for unlimited large requests and standardized oversized-request responses.

…m 413 surface

The reference LLM proxy ships with no request-body cap (its request-size
guard defaults to off), and providers accept larger requests than any
fixed gateway default — Anthropic takes 32 MB — so the 10 MiB default
rejected requests the upstream would have served.

- Default 0 = cap disabled: DefaultBodyLimit::disable() (axum would
  otherwise fall back to its built-in 2 MiB), Content-Length pre-check
  skipped, manual to_bytes reads widened. The duplicate-Content-Length
  rejection stays on — smuggling hygiene, not a size limit.
- Configured caps behave exactly as before, and the 413 surface is now
  uniform on the chunked path: five bare Json handlers + the two Bytes
  handlers map through the shared rejection helpers, multipart keeps
  axum's 413 classification instead of folding it into 400, and
  /mcp + /a2a discriminate the length-limit error into the standard
  envelope.
- e2e: harness gets a dedicated requestBodyLimitBytes override (pins
  10 MiB for the existing 413 suite); new suite proves a 12 MiB body
  reaches the upstream on the new default.
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Request-body limits now default to unlimited when configured as 0. Router, extractor, multipart, JSON-RPC, and passthrough paths classify oversized bodies consistently and return enveloped 413 errors. Unit, integration, and E2E tests cover disabled limits and oversized requests.

Changes

Request body limit handling

Layer / File(s) Summary
Configurable body-limit defaults and enforcement
config.example.yaml, config.managed.yaml, crates/aisix-core/src/config.rs, crates/aisix-proxy/src/lib.rs
The default body limit is changed to 0; router extraction and declared Content-Length enforcement disable size rejection when the cap is zero while duplicate headers remain rejected.
Standardized extractor and multipart errors
crates/aisix-proxy/src/error.rs, crates/aisix-proxy/src/audio.rs, crates/aisix-proxy/src/completions.rs, crates/aisix-proxy/src/images.rs, crates/aisix-proxy/src/jobs.rs, crates/aisix-proxy/src/rerank.rs, crates/aisix-proxy/src/responses.rs
Request extraction and multipart failures now map through shared proxy error helpers, distinguishing oversized requests from other invalid-body errors.
Capped buffering for protocol and passthrough routes
crates/aisix-proxy/src/a2a.rs, crates/aisix-proxy/src/mcp.rs, crates/aisix-proxy/src/passthrough.rs
A2A, MCP, and passthrough body reads use the shared cap behavior and return distinct responses for length-limit and non-limit failures.
Limit behavior regression coverage
crates/aisix-proxy/src/lib.rs, tests/e2e/src/cases/body-edges-e2e.test.ts, tests/e2e/src/harness/app.ts
Tests cover zero-limit behavior, chunked and multipart oversize responses, duplicate headers, passthrough requests, and delivery of a large unrestricted body.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant ProxyMiddleware
  participant AxumBodyExtractor
  participant ProxyError
  Client->>ProxyMiddleware: Send request body
  ProxyMiddleware->>AxumBodyExtractor: Apply configured body cap
  AxumBodyExtractor->>ProxyError: Report length-limit rejection
  ProxyError-->>Client: Return enveloped 413 response
Loading

Possibly related PRs

  • api7/aisix#816: Also modifies the /v1/completions handler, although it changes upstream retry dispatch rather than request-body error handling.

Suggested reviewers: moonming

🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
E2e Test Quality Review ⚠️ Warning The E2E suite covers Content-Length and unlimited-default paths, but not the chunked/no-Content-Length regression this PR fixed, leaving a critical flow untested. Add an E2E case that sends the oversized body chunked/no Content-Length and asserts the OpenAI 413 envelope; move unrelated chat-validation cases to a separate suite.
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: defaulting request_body_limit_bytes to 0 and standardizing 413 responses.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Security Check ✅ Passed PASS: The touched code only adjusts body-cap handling; no new secret leakage, auth bypass, or ownership/TLS issues were introduced.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/body-limit-unlimited-default

Comment @coderabbitai help to get the list of available commands.

…ore #849

Audit follow-ups on the unlimited-default change:

- The passthrough tunnel's manual to_bytes still used the raw limit, so
  the new 0 default rejected EVERY passthrough body with
  '413 request body exceeds 0-byte limit' (reproduced by the audit).
  It now goes through body_read_cap and discriminates the length-limit
  error from transport faults — a real cap hit is the enveloped 413, a
  read failure is a 400, matching /mcp and /a2a. Regression tests for
  both, plus chunked-oversize coverage for /mcp and /a2a.
- Restores Cargo.toml strip = 'debuginfo' and the docker-image.yml
  symbol-table guard from #849: the previous commit was assembled with
  'git reset --soft origin/main' after the remote ref had advanced past
  the working tree's base, so its tree silently reverted that commit.
  No intended change there.
@jarvis9443

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
crates/aisix-core/src/config.rs (1)

376-401: 🔒 Security & Privacy | 🔵 Trivial

Default request-body cap now "unlimited" — confirm this is called out to operators.

default_body_limit() changing from 10 MiB to 0 (disabled) is a deliberate design choice matching upstream LLM proxy behavior, and it's documented thoroughly in the doc comment. Worth flagging explicitly for release notes: any operator upgrading without setting request_body_limit_bytes will silently go from a 10 MiB cap to no cap at all on JSON/multipart/passthrough/MCP/A2A bodies. Axum's own DefaultBodyLimit::disable() docs recommend pairing this with an independent limit (e.g. tower_http::limit) "if you're accepting data from untrusted remotes" — worth considering whether a reverse-proxy/LB in front of the gateway is expected to be the actual backstop, and documenting that assumption.
Note that if you’re accepting data from untrusted remotes it is recommend to add your own limit such as tower_http::limit.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aisix-core/src/config.rs` around lines 376 - 401, Update
operator-facing release notes/documentation for ProxyConfig::default_body_limit
to explicitly state that the default request_body_limit_bytes is now unlimited
instead of 10 MiB across all supported request types. Document that deployments
accepting untrusted remotes must provide an independent backstop, such as a
reverse-proxy/LB limit or tower_http::limit, rather than relying on the disabled
default.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@crates/aisix-core/src/config.rs`:
- Around line 376-401: Update operator-facing release notes/documentation for
ProxyConfig::default_body_limit to explicitly state that the default
request_body_limit_bytes is now unlimited instead of 10 MiB across all supported
request types. Document that deployments accepting untrusted remotes must
provide an independent backstop, such as a reverse-proxy/LB limit or
tower_http::limit, rather than relying on the disabled default.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 42a7362b-12c4-4b2d-b131-54b27a873b86

📥 Commits

Reviewing files that changed from the base of the PR and between 0866202 and e544a51.

📒 Files selected for processing (16)
  • config.example.yaml
  • config.managed.yaml
  • crates/aisix-core/src/config.rs
  • crates/aisix-proxy/src/a2a.rs
  • crates/aisix-proxy/src/audio.rs
  • crates/aisix-proxy/src/completions.rs
  • crates/aisix-proxy/src/error.rs
  • crates/aisix-proxy/src/images.rs
  • crates/aisix-proxy/src/jobs.rs
  • crates/aisix-proxy/src/lib.rs
  • crates/aisix-proxy/src/mcp.rs
  • crates/aisix-proxy/src/passthrough.rs
  • crates/aisix-proxy/src/rerank.rs
  • crates/aisix-proxy/src/responses.rs
  • tests/e2e/src/cases/body-edges-e2e.test.ts
  • tests/e2e/src/harness/app.ts

@jarvis9443

Copy link
Copy Markdown
Contributor Author

Addressed the release-note/backstop point in the paired docs PR (api7/docs#1987): the reference page now carries an explicit upgrade note (earlier releases defaulted to 10 MiB) and tells operators accepting untrusted clients directly to either set a value or enforce the limit at the LB/ingress in front. The PR description's Behaviour changes section covers the same for release notes.

@jarvis9443
jarvis9443 merged commit e1da81d into main Jul 30, 2026
12 checks passed
@jarvis9443
jarvis9443 deleted the feat/body-limit-unlimited-default branch July 30, 2026 10:41
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.

1 participant