Skip to content

fix(obs): log the requests refused before dispatch - #863

Merged
jarvis9443 merged 2 commits into
mainfrom
feat/access-log-pre-dispatch-rejections
Jul 31, 2026
Merged

fix(obs): log the requests refused before dispatch#863
jarvis9443 merged 2 commits into
mainfrom
feat/access-log-pre-dispatch-rejections

Conversation

@jarvis9443

Copy link
Copy Markdown
Contributor

Problem

A request whose body exceeds request_body_limit_bytes gets a correct 413 and leaves no trace in the gateway. The access log and aisix_requests_total are emitted by the handler at the end of dispatch — that is the only place that knows provider, model and token counts — and a body-cap rejection never gets there:

  • Declared Content-Length over the cap (what every non-streamed SDK POST sends): enforce_request_body_limit short-circuits before any handler runs.
  • Chunked / lying Content-Length: the handler's body extractor rejects and the handler returns at its first statement.

So "the client reports a 413 the gateway has no record of" was indistinguishable from the request never arriving. The conflicting-Content-Length 400 had the same hole.

Change

One helper — reject::reject_before_dispatch — renders the caller's envelope and emits the access log + request metrics, so the two can't drift apart. Every pre-dispatch rejection answers through it:

  • middleware: the 413 short-circuit and the conflicting-Content-Length 400
  • handlers: chat, completions, embeddings, responses, rerank, images, audio speech, messages, count_tokens, batches, fine-tuning jobs

/mcp, /a2a, /passthrough, /v1/videos and /v1/files wrap their whole dispatch and already recorded these — untouched, and adding a second emit there would double-log.

chat.rs and embeddings.rs also drop their inlined copies of the 413-vs-400 discrimination for the shared proxy_error_from_json_rejection the sibling routes already use.

Behaviour

Nothing changes on the wire: same status, same envelope, same body. What is new is one access-log line and one aisix_requests_total{provider="unknown",model="unresolved",status="413"} sample per refused request.

  • The middleware runs ahead of authentication, so its lines carry no api_key_id; the handler-side ones do.
  • The logged latency spans the body drain — what an oversize request actually costs the gateway.
  • No usage event: the middleware has no authenticated key to attribute one to, so /logs and the budget ledger are unchanged.
  • error_kind stays the coarse invalid_request_error the OpenAI envelope pins; the cap hit is named in error.

Tests

  • unit: both paths — the middleware short-circuit and the handler-side chunked rejection — now produce a status="413" sample. The metric rather than the log line, because the two are emitted by the same helper while the line rides process-global tracing state that a parallel unit test can lose.
  • E2E (body-edges): the oversize request's access-log line is joined to the caller's x-aisix-request-id and checked for status, path and reason, plus the metric; a second case covers the chunked path, whose refusal is recorded by the handler.
  • The E2E harness gains a logLevel override: the access log is an info-level line and the harness pins RUST_LOG=warn, which silently outranks observability.log_level.

Mainstream gateways short-circuit an oversize request the same way and rely on their HTTP server's built-in access log to record it; this gateway has no server-level access log, so it has to emit the line itself.

A body-cap rejection is answered either by the `enforce_request_body_limit`
middleware (declared Content-Length over the cap — every non-streamed SDK
POST) or by a handler's body extractor (chunked / lying Content-Length).
Both return before the dispatch tail that emits the access log and
`record_request`, so the caller got a 413 the gateway kept no record of:
no access-log line, no metric sample. "Client reports 413, gateway shows
nothing" is indistinguishable from the request never arriving.

Route every pre-dispatch rejection through one helper (`reject.rs`) that
renders the caller's envelope AND emits the telemetry, so the two can't
drift apart: the middleware's two short-circuits (413 and the conflicting
Content-Length 400) plus the extractor rejections in chat, completions,
embeddings, responses, rerank, images, audio speech, messages,
count_tokens, batches and fine-tuning jobs. The endpoints that already
wrap their whole dispatch — /mcp, /a2a, /passthrough, /v1/videos,
/v1/files — logged these correctly and are untouched.

The middleware runs ahead of authentication, so its lines carry no
api_key_id; the handler-side ones do. Latency spans the body drain, which
is what an oversize request actually costs the gateway. Nothing on the
wire changes: same status, same envelope. Response bodies stay out of the
log — only method, path, status, latency, request id and the failure.

chat.rs and embeddings.rs also drop their inlined copies of the
413-vs-400 discrimination for the shared `proxy_error_from_json_rejection`
the sibling routes already use.

The E2E harness gains a `logLevel` override, because the access log is an
info-level line and the harness pins RUST_LOG=warn — which silently
outranks `observability.log_level`.
The unit assertion moves from the access-log line to the request metric:
both are emitted by the same helper, but the log line rides process-global
tracing state, so a parallel unit test can lose it to another test's
subscriber guard. The line itself is pinned end-to-end in the body-edges
E2E instead — now for the chunked path too, whose refusal is recorded by
the handler rather than the middleware.
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 34 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 29b8a8d3-6f4b-45e3-9be2-f252633c9905

📥 Commits

Reviewing files that changed from the base of the PR and between 8e7b9cb and 4a58053.

📒 Files selected for processing (15)
  • crates/aisix-proxy/AGENTS.md
  • crates/aisix-proxy/src/audio.rs
  • crates/aisix-proxy/src/chat.rs
  • crates/aisix-proxy/src/completions.rs
  • crates/aisix-proxy/src/count_tokens.rs
  • crates/aisix-proxy/src/embeddings.rs
  • crates/aisix-proxy/src/images.rs
  • crates/aisix-proxy/src/jobs.rs
  • crates/aisix-proxy/src/lib.rs
  • crates/aisix-proxy/src/messages.rs
  • crates/aisix-proxy/src/reject.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

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

@jarvis9443
jarvis9443 merged commit 1df7716 into main Jul 31, 2026
10 checks passed
@jarvis9443
jarvis9443 deleted the feat/access-log-pre-dispatch-rejections branch July 31, 2026 12:00
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