Skip to content

fix(proxy): route Path-extractor rejections through the reject chokepoint - #884

Merged
jarvis9443 merged 2 commits into
mainfrom
feat/path-rejection-telemetry
Aug 4, 2026
Merged

fix(proxy): route Path-extractor rejections through the reject chokepoint#884
jarvis9443 merged 2 commits into
mainfrom
feat/path-rejection-telemetry

Conversation

@jarvis9443

@jarvis9443 jarvis9443 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

What

A :param segment that fails path extraction — valid percent-encoding that decodes to invalid UTF-8, like /v1/files/%ff — answered axum's bare 400 from inside the extractor: no access log line, no request metrics, no caller envelope. That is the same silent pre-dispatch class #863 collected into crate::reject for body rejections: the caller sees an error the operator has no record of.

New AisixPath<T> extractor (in reject.rs, next to the chokepoint it feeds) wraps axum::extract::Path and routes the rejection through reject_before_dispatch:

  • caller-caused 400s render the OpenAI error envelope (invalid_request_error) and emit the access log + request metrics every other pre-dispatch rejection gets (unresolved provider/model labels — cardinality unchanged; the raw path only reaches the access log);
  • axum's 500-class path rejections (wrong parameter arity / unsupported type — server wiring bugs) keep axum's own loud 500 instead of being recorded as caller input;
  • the resolved key published by the auth extractor attributes the rejection (api_key_id).

Every :param handler takes AisixPath instead of Path — 13 sites across /a2a/:agent (+card), /mcp/:server, /v1/files/:id (+content/DELETE), /v1/batches/:id (+cancel), /v1/fine_tuning/jobs/:id (+cancel), /v1/videos/:id (+content), and /passthrough/:provider/*rest. Behavior for valid parameters is byte-identical (pure delegation to axum's Path); extractor order is unchanged, so authentication still precedes the path parse.

Same class, one extractor over (review follow-up): JSON sent to a multipart endpoint — /v1/audio/transcriptions, /v1/audio/translations, /v1/files upload — was the identical silent bare-400; all three now route through the chokepoint. The WebSocketUpgrade rejection on /v1/realtime is deferred to #885: its correct refusal is 426 Upgrade Required and ProxyError has no such variant, so folding it in here would mis-report the status.

Envelope note: all touched routes speak the OpenAI error shape; the Anthropic-envelope routes (/v1/messages*) carry no path parameters and no multipart.

Tests

  • Router-level (reject.rs): the whole :param family — 13 malformed-path requests assert 400 + envelope and that the refusals are counted (metrics scrape with unresolved labels); the three multipart routes assert the envelope on a content-type mismatch; a deliberately mis-wired route pins that arity bugs keep their 500 without the caller envelope; auth-precedes-parse pinned.
  • E2E (path-param-reject-e2e.test.ts): real binary + etcd — every :param family member answers the envelope on %ff, the multipart mismatch answers it too, and the unauthenticated variant stays 401-first.

No LiteLLM baseline applies: gateway-internal rejection telemetry/envelope consistency with no equivalent surface there.

Fixes #880.

…oint

A ':param' segment that fails extraction — valid percent-encoding that
is invalid UTF-8 after decoding, like /v1/files/%ff — answered axum's
bare 400: no access log, no request metrics, no caller envelope. The
same silent pre-dispatch class #863 collected for body rejections.

New AisixPath extractor wraps axum's Path and routes the rejection
through reject_before_dispatch, so the refusal gets the OpenAI envelope
plus the access log + metrics every other pre-dispatch rejection gets;
the resolved key published by the auth extractor attributes it. Every
':param' handler (a2a, mcp scoped, files, batches, fine-tuning jobs,
videos, passthrough — 13 sites) takes AisixPath instead of Path, so the
family can't drift back. Extractor order is unchanged: authentication
still precedes the path parse, pinned by test.

Fixes #880.
@coderabbitai

coderabbitai Bot commented Aug 4, 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: 54 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: fdb1f64c-347d-4679-9291-4e5e52cdb1dc

📥 Commits

Reviewing files that changed from the base of the PR and between 7d519b7 and 39d33a4.

📒 Files selected for processing (8)
  • crates/aisix-proxy/src/a2a.rs
  • crates/aisix-proxy/src/audio.rs
  • crates/aisix-proxy/src/jobs.rs
  • crates/aisix-proxy/src/mcp.rs
  • crates/aisix-proxy/src/passthrough.rs
  • crates/aisix-proxy/src/reject.rs
  • crates/aisix-proxy/src/videos.rs
  • tests/e2e/src/cases/path-param-reject-e2e.test.ts

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

…ections too

Independent review findings:

- The extractor rendered every PathRejection as a caller 400, including
  axum's 500-class programmer errors (wrong parameter arity, unsupported
  type). Those keep axum's own loud 500 now — a wiring bug must not be
  recorded as caller input — pinned by a deliberately mis-wired test
  route.
- The same silent bare-400 class existed one extractor over: JSON sent
  to a multipart endpoint (/v1/audio/transcriptions, /v1/audio/
  translations, /v1/files) bypassed the envelope and telemetry the same
  way. All three now route through the chokepoint. The WebSocketUpgrade
  rejection on /v1/realtime is deferred to #885: its correct status is
  426 and ProxyError has no such variant yet.
- The family test now also asserts the refusals are counted (metrics
  scrape with the unresolved labels), covers DELETE /v1/files/:id, and
  the request-id fallback carries a comment naming why it mirrors the
  handlers' idiom.
@jarvis9443
jarvis9443 merged commit eca3f2a into main Aug 4, 2026
12 checks passed
@jarvis9443
jarvis9443 deleted the feat/path-rejection-telemetry branch August 4, 2026 12:02
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.

Route Path-extractor rejections through the pre-dispatch reject chokepoint on :param endpoints

1 participant