Skip to content

fix(gateway): give the request body limit one source and raise it to 277 MiB - #272

Merged
argszero merged 1 commit into
mainfrom
fix/gateway-body-limit-277m
Sep 21, 2026
Merged

argszero merged 1 commit into
mainfrom
fix/gateway-body-limit-277m

Conversation

@argszero

Copy link
Copy Markdown
Owner

Summary

The request body limit was stored on two layers with two numbers, and the smaller one silently wins. This raises the gateway limit from 8 MiB to 277 MiB and gives the number a single source, so the two layers can no longer disagree.

Continues rant 2026-09-18T09:14:18 ("the configured limit is not the effective limit").

Related Issue

rant 2026-09-18T09:14:18.500174+08:00 (request body limit). Its v0.7.26 half moved the limit to the layer axum actually reads; this PR covers the follow-up observed on dev, where a 10 MB request was still rejected.

Changes

The limit lives on two layers:

Layer Mechanism Behaviour
axum extractor DefaultBodyLimit::max(…) — the only writer of the DefaultBodyLimitKind extension that overrides axum-core's 2 MiB DEFAULT_LIMIT the real cap for String / Json bodies
tower-http outer guard RequestBodyLimitLayer::new(…) answers 413 without reading the body when Content-Length exceeds its own number

The outer layer cannot raise the inner one (it never writes that extension — that was the original defect), but it clamps it from below whenever it is smaller. That is exactly what would have happened here: with the constant raised to 277 MiB and the outer layer still carrying its own 70 * 1024 * 1024, the effective limit would have been 70 MB, not 277 MiB. Measured on the pre-fix tree: a 71 MiB body returned 413 length limit exceeded — tower-http's own body text, not axum's Failed to buffer the request body: ….

  • GATEWAY_BODY_LIMIT: 8 MiB → 277 MiB, with the two-layer trap documented at the constant.
  • The outer layer now takes GATEWAY_BODY_LIMIT too, and is constructed in routes::router() instead of main.rs — the router is what the tests build, so the tested stack is the production stack.
  • New test-only gate src/body_limit_gate.rs (#[cfg(test)] mod, not in the release binary, no new dependencies): every layer constructor must receive GATEWAY_BODY_LIMIT, and no second …BODY_LIMIT constant may exist anywhere under src/.
  • Probe sizes: the positive probe moves 3 MiB → 9 MiB (crossing the old 8 MiB, so it is a regression leg for this raise), and a new 71 MiB leg fails on the pre-fix outer layer. The unauthenticated-endpoint negative control is unchanged (2 MiB default deliberately kept).
  • CHANGELOG.md is untouched: by convention it is written only by chore(release) PRs.

Tests

cargo test                       # 308 passed; 0 failed   (main: 302 -> 308, +1 behavioural, +5 gate)
cargo fmt --check                # clean
cargo clippy --all-targets -- -D warnings   # clean

Mutation matrix (each leg declares the expected red set; working tree restored byte-exactly, md5-verified):

mutant red
base (this PR) —
outer layer back to a literal 70 * 1024 * 1024 the_outer_limit_does_not_clamp_below_the_gateway_limit, body_limit_gate::every_body_limit_layer_is_the_same_single_number
constant back to 8 MiB gateway_routes_accept_bodies_past_the_default_limit, the_outer_limit_does_not_clamp_below_the_gateway_limit
a second constant name (SECOND_BODY_LIMIT) body_limit_gate::every_body_limit_layer_is_the_same_single_number
outer layer deleted entirely body_limit_gate::every_body_limit_layer_is_the_same_single_number (call-site positive control)

Measured witness for the outer clamp, on the mutant that keeps 70 * 1024 * 1024:

assertion `left != right` failed: 外层把 74448896 字节的体抢答成 413 了
  (外层 limit 与 GATEWAY_BODY_LIMIT 不同源):413 Payload Too Large length limit exceeded

⚠️ Not yet verified end to end — the dev endpoint still runs v0.7.26; a real 71 MiB / 100 MiB request against a deployed build has not been run yet.

⚠️ Memory: the extractor buffers the whole body in memory, and chat_completions clones it once for the forward, so a single request peaks at roughly 2–3× this number. The prod host advertises 1.8 GiB total with ~0.57 GiB available, no swap, and the application container has no mem_limit. A 277 MiB request is therefore a host-level OOM risk there, not just a container one.

Checklist

  • Tests pass (cargo test)
  • cargo fmt --check and cargo clippy -- -D warnings pass
  • No new dependency (the gate is std-only, test-only)
  • Outward-facing text in English

…277 MiB

The limit lives on two layers: axum's extractor (`DefaultBodyLimit::max`, the only
writer of the `DefaultBodyLimitKind` extension that overrides the 2 MiB
`DEFAULT_LIMIT`) and tower-http's outer guard (`RequestBodyLimitLayer::new`, which
answers 413 without reading the body when `Content-Length` exceeds its own number).

The outer layer cannot raise the inner one, but it wins whenever it is smaller.
Raising the constant from 8 MiB to 277 MiB while the outer layer still carried
`70 * 1024 * 1024` would have capped the effective limit at 70 MB - measured, a
71 MiB body came back `413 length limit exceeded` (tower-http's own body text, not
axum's `Failed to buffer the request body: ...`).

- `GATEWAY_BODY_LIMIT` 8 MiB -> 277 MiB, and the outer layer now references that
  same constant instead of a literal of its own
- move the outer layer into `routes::router()` (it was built in `main.rs` with its
  own number) so the stack the tests exercise is the production stack
- new gate `src/body_limit_gate.rs`: every layer constructor must take
  `GATEWAY_BODY_LIMIT`, and no second `...BODY_LIMIT` constant may exist
- probe sizes: 3 MiB -> 9 MiB (crosses the old 8 MiB) plus a new 71 MiB leg that
  fails on the pre-fix outer layer; the login negative control is unchanged

Tests: 302 -> 308 passed, fmt/clippy clean.
@argszero

Copy link
Copy Markdown
Owner Author

✅ LGTM (self-review)

Verified on the branch head:

  • cargo test → 308 passed / 0 failed (main is 302; +1 behavioural, +5 gate).
  • cargo fmt --check and cargo clippy --all-targets -- -D warnings → clean.
  • Mutation matrix, declarative (each leg states its expected red set, tree restored byte-exactly and md5-verified): the literal 70 * 1024 * 1024 outer layer, the constant lowered back to 8 MiB, a second …BODY_LIMIT name, and deleting the outer layer all go red exactly where declared.
  • The clamp is measured, not inferred: on the mutant that keeps the outer literal, a 71 MiB body returns 413 Payload Too Large with body length limit exceeded — tower-http's own text, which is what pins the source of the 413 to the outer layer rather than the extractor.

Two notes recorded in the PR body rather than fixed here:

  1. Not yet verified end to end on a deployed build (dev still runs v0.7.26).
  2. Memory: one request peaks at roughly 2–3× the limit (extractor buffer + the handler's clone for the forward). The prod host has 1.8 GiB total / ~0.57 GiB available, no swap, and the container declares no mem_limit. That host exposes only 80/443 to the internet (verified: 8085 and 8083 are unreachable from outside), and the public path is capped at nginx's 1 MB — so this is not remotely reachable today, but it should be settled before the public path is opened at this size.

@argszero
argszero merged commit 87c8777 into main Sep 21, 2026
1 check passed
@argszero
argszero deleted the fix/gateway-body-limit-277m branch September 21, 2026 12:20
@argszero argszero mentioned this pull request Sep 21, 2026
4 tasks done
argszero added a commit that referenced this pull request Sep 21, 2026
Request body limit follows the single-constant refactor from #272.
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