Skip to content

fix(gateway): apply the body limit where axum reads it (per-route DefaultBodyLimit) - #267

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

argszero merged 1 commit into
mainfrom
fix/request-body-limit

Conversation

@argszero

Copy link
Copy Markdown
Owner

Summary

The gateway's long-context support has never actually been in effect. src/main.rs:239 installs RequestBodyLimitLayer::new(70 * 1024 * 1024), and CHANGELOG.md (v0.7.10) records the limit as "raised to 70MB" — but the effective cap has always been axum's built-in 2 MiB (axum-core 0.4.5, DEFAULT_LIMIT = 2_097_152). Every request to /v1/chat/completions, /anthropic/v1/messages or /v1/responses whose body exceeds 2 MiB is rejected with 413 Failed to buffer the request body: length limit exceeded inside the extractor, before the handler runs. For a ~1M-token JSON context (≈3–4 MB) that meant an effective ceiling of roughly a quarter of the intended context, and any base64 image payload over 2 MiB was refused outright.

Why the installed layer is inert: RequestBodyLimitLayer (tower-http) and DefaultBodyLimit (axum) are not the same mechanism. The extractors' limit comes from axum_core::extract::Request::with_limited_body(), which only reads the DefaultBodyLimitKind request extension — and only axum's own DefaultBodyLimit writes that extension. The tower-http layer never does, so the 70 MB number was decorative.

Measured against an identical layering (separate throwaway crate, axum 0.7.9, cargo build --offline), posting raw bodies to the same route shapes:

shape /v1/chat/completions 2 MiB +1 byte 3 MB 5 MB /api/auth/login 3 MB
current (global RequestBodyLimitLayer(70MB) only) 200 413 413 413 413
this PR (per-route DefaultBodyLimit::max(8 MiB)) 200 200 200 200 413
the same fix written as a sub-Router + merge 200 200 200 200 413

The last two rows are byte-identical, so the minimal-diff form (MethodRouter::layer) was chosen on evidence rather than on idiom.

The same defect is reproduced inside this crate, not only in the throwaway one: with the route layers reverted and only the new tests applied, gateway_routes_accept_bodies_past_the_default_limit fails on /v1/chat/completions with 413 — the real router, the real extractor, a real 3 MiB body.

Related Issue

No issue. Host rant 2026-09-18T09:14:18.500174+08:00 — 「【部署/网关】请求体上限实际只有 1MB(prod 公网)/ 2MB(应用),代码里写的 70MB 从未生效」.

⚠️ The rant has two halves; this PR covers only the application half. The public entry point (prod nginx-proxy, client_max_body_size defaulting to 1 MB for aitokenpool.args.fun) is a host deployment action and is deliberately out of scope here — without it, raising the app limit to 8 MiB still leaves the public endpoint capped at 1 MB.

Changes

  • src/routes/mod.rs
    • new pub(crate) const GATEWAY_BODY_LIMIT: usize = 8 * 1024 * 1024; with a comment recording why main.rs's tower-http layer cannot be used for this.
    • the three gateway routes now carry .layer(DefaultBodyLimit::max(GATEWAY_BODY_LIMIT)).
    • per-route, not global: the unauthenticated endpoints (/api/auth/*) stay at the 2 MiB default, so an anonymous request cannot make the process buffer 8 MiB.
  • src/gateway.rs: two tests in the existing tests module.
    • gateway_routes_accept_bodies_past_the_default_limit — 3 MiB body on each of the three gateway routes must not be 413, and must reach the handler (it asserts the gateway's own 暂无可用 key response for a non-existent model, which proves the body was read and routed).
    • auth_endpoints_stay_at_the_default_limit — negative control: /api/auth/login with a 3 MiB body must still be 413.
  • No config or data-structure change, so config.example.toml needs no counterpart.
  • CHANGELOG.md is deliberately not touched: only chore(release) PRs write it (git log -- CHANGELOG.md — the last 12 commits are all chore(release)). The false v0.7.10 entry ("Request body limit raised to 70MB") is a candidate for correction by the next release PR; flagging it here rather than editing it in a fix PR.

Tests

  • cargo test299 passed / 0 failed on this branch (baseline main a4cb622 = 297; the difference is exactly the 2 tests added here)
  • A/B measured inside the real crate, not only in a synthetic one: with only src/gateway.rs applied (the route layers reverted) gateway_routes_accept_bodies_past_the_default_limit fails with 413 on /v1/chat/completions while auth_endpoints_stay_at_the_default_limit still passes; re-applying the route layers turns both green
  • cargo clippy --all-targets — no warnings on this tree
  • cargo fmt --check (both edited files verified with rustfmt --check --edition 2021)
  • new/updated unit tests: 2 added

Checklist

  • branch name follows the convention (fix/request-body-limit)
  • Conventional Commits message
  • single responsibility, minimal diff (2 files; the three .route(...) lines plus an import, a const and two tests)

…aultBodyLimit)

`RequestBodyLimitLayer(70MB)` in main.rs never took effect: the extractor cap is
axum-core's DEFAULT_LIMIT (2 MiB) and only axum's own DefaultBodyLimit sets the
extension that overrides it. Raise the three gateway routes to 8 MiB, leave the
unauthenticated endpoints at the default, and pin both with tests.

rant 2026-09-18T09:14:18
@argszero

Copy link
Copy Markdown
Owner Author

Self-review (Committer, allow_self_merge: true — recorded as a comment because GitHub refuses a formal --approve on one's own PR).

Checked before merging:

  • CI green on the head 2d51fa7: test / fmt / clippy passed (run 35583058530).
  • The fix is applied where axum actually reads the limit. RequestBodyLimitLayer (tower-http) wraps the outer body stream; only axum's own DefaultBodyLimit writes the DefaultBodyLimitKind request extension that axum_core::extract::Request::with_limited_body() consults, so the pre-existing 70 MB layer was inert and the effective cap stayed DEFAULT_LIMIT = 2_097_152. This patch sets the extension on exactly the three routes that need it.
  • Scope is the minimum that fixes it. Per-route rather than global: /api/auth/* and the other unauthenticated endpoints keep the 2 MiB default, so an anonymous request cannot make the process buffer 8 MiB. The negative-control test pins that, so a future "just raise it globally" change turns red.
  • The tests cannot pass by accident. The positive test asserts a non-413 and that the response is the gateway's own 暂无可用 key (i.e. the body was read and routed to the handler), rather than merely that the status changed; the negative control asserts an exact 413 on /api/auth/login.
  • Discrimination measured, not assumed. With the route layers reverted and only the test file applied, gateway_routes_accept_bodies_past_the_default_limit fails with 413 on /v1/chat/completions; re-applying them turns it green. cargo test 299 passed / 0 failed (baseline 297) — the delta is exactly the two tests added.
  • Not in this PR, deliberately. CHANGELOG.md (only chore(release) PRs write it) and the prod nginx client_max_body_size half of the rant, which is a host deployment action.

@argszero
argszero merged commit 2ccbc76 into main Sep 21, 2026
1 check passed
@argszero
argszero deleted the fix/request-body-limit branch September 21, 2026 09:26
@argszero argszero mentioned this pull request Sep 21, 2026
12 tasks
argszero added a commit that referenced this pull request Sep 21, 2026
Ships the 10 PRs merged since v0.7.25 (#261-#270). No schema change, no config
change, so the deployment-side config.toml needs no edit.

One fact, one source / display must equal what it consumes (frontend, 6 places)
- #261 read the spendable half of the wallet payload when refreshing your own
  balance; #262 the transactions payload signature covers the time range, with
  one reload trigger shared by the four controls; #263 the settings controls are
  either wired or explicitly inert; #265 the re-list outcome comes from the same
  entry as its action; #268 the sharing form shows a plan's label, not its
  config id; #269 the ops card stops reading a key's status count as a health
  verdict.

i18n reachability
- #266 every pack key must reach a consumer (the gate), and #270 drops the 23
  keys that gate proved unreachable: ZH/EN key count 811 -> 788, sunset list
  59 -> 36.

Gateway
- #267 applies the body limit where axum actually reads it (per-route
  DefaultBodyLimit, 8 MiB on the three gateway routes; unauthenticated
  endpoints keep the 2 MiB default). This is the application half of rant
  2026-09-18T09:14:18. It also corrects the false v0.7.10 "raised to 70MB"
  CHANGELOG line, which described installing a layer rather than raising a limit.

- Cargo.toml / Cargo.lock: 0.7.25 -> 0.7.26.
- CHANGELOG.md: v0.7.26 entry plus the v0.7.10 correction.
- ui/index.html cache-bust left as-is: this release touches no UI file; the live
  values are app.js 20260921-2 / i18n.js 20260921-2.

cargo test 302 passed; cargo fmt --check clean; clippy -D warnings clean.
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