test(e2e): C7 /passthrough/anthropic auth-shape switching + verbatim forward - #165

Merged
moonming merged 2 commits into
mainfrom
test/e2e-c7-passthrough
May 9, 2026
Merged

test(e2e): C7 /passthrough/anthropic auth-shape switching + verbatim forward#165
moonming merged 2 commits into
mainfrom
test/e2e-c7-passthrough

Conversation

@moonming

@moonmingmoonming commented May 9, 2026

Copy link
Copy Markdown
Member

Summary

Third endpoint covered from #151's C7 row. Per gateway docs §4.10, `/passthrough/{provider}/*rest` is the lowest-overhead escape hatch for provider endpoints aisix doesn't natively wrap (OpenAI batches / files / fine-tuning, Anthropic message-batches). Prior to this PR the gateway had zero e2e coverage on `/passthrough`.

One of two originally-planned cases ships here. The other surfaced a docs/behavior conflict and is held back as #164.

What's pinned

CaseUser journeyAsserts
Anthropic passthroughPOST `/passthrough/anthropic/v1/messages/batches` with arbitrary bodyCaller sees upstream's response back; upstream got POST at `/v1/messages/batches` (path verbatim after `/passthrough/anthropic` stripped); auth-shape switching: `x-api-key: sk-ant-mock` (NOT caller's bearer) + `anthropic-version: 2023-06-01` (Anthropic's documented current version); upstream Authorization (if present) does not contain caller's plaintext bearer (no credential leak); body forwarded byte-for-byte

Held back as separate issues

🐛 #164 — `/passthrough/openai/*` doubles `/v1` prefix when api_base ends with `/v1` (contradicts published docs). `docs/api-admin.md` §4.3 publishes `api_base: "https://api.openai.com/v1"\`; `docs/api-proxy.md` §4.10 publishes the call as `/passthrough/openai/v1/batches`. Together: `/v1/v1/batches` upstream. OpenAI passthrough test will return once reconciled.

🐛 #166 — `/passthrough/anthropic` injects redundant `Authorization: Bearer` alongside x-api-key. NOT a credential leak (Bearer value is gateway's own provider secret, not caller's plaintext) but a wire-cleanliness gap vs docs §4.10 implication of x-api-key + anthropic-version only for Anthropic. Test pins the leak concern explicitly; the strict "Authorization absent" assertion is the verification once #166 is resolved.

Why this matters

Three real-world failure modes that prior coverage couldn't catch:

  • Auth-shape regression — a regression that forwarded `Authorization: Bearer` to Anthropic upstream as the only auth header would 401 in production.
  • Caller's bearer leak — the test verifies that even when Authorization IS present (per bug: /passthrough/anthropic injects redundant Authorization: Bearer <provider-secret> alongside x-api-key #166's wire-cleanliness gap), it does NOT contain the caller's plaintext bearer.
  • Body normalization regression — passthrough's whole point is "we don't normalize". A regression that JSON-round-tripped or schema-validated bodies on the way through would silently drop unknown fields, breaking batches/files use cases that depend on free-form bodies.

Source-blind discipline

Every assertion derives from external contracts:

No internal Rust paths or struct field names referenced.

Independent audit

Per CLAUDE.md §8, an independent audit agent reviewed the initial commit (4ffd636). Resolution log:

FindingSeverityResolution
Caller-bearer-leak assertion missing despite being a stated motivationMEDIUMTightened: upstream Authorization (if present) must not contain caller's plaintext bearer (7a2ed99)
`anthropic-version` pinned only as "non-empty string" — would pass on malformed values like `"v1"`MEDIUMPinned exact value `"2023-06-01"` per Anthropic docs (7a2ed99)
HTTP method matrix coverage gap (route is `ANY`; only POST tested)MEDIUMAcknowledged scope deferral
Provider matrix coverage gap (only anthropic; gemini/deepseek not tested)MEDIUMAcknowledged scope deferral; OpenAI held back as #164
Body verbatim assertion is string-equalityMEDIUMDeliberate strict contract per docs §4.10 — no change
L1-L3 informationalLOWNot addressed

The audit's recommended strict assertion (`Authorization === undefined`) revealed a separate gateway-side issue (#166): NOT a caller-bearer leak, but a redundant Bearer injection alongside x-api-key. The shipped assertion pins the leak concern explicitly while #166 tracks the wire-cleanliness gap.

Test plan

  • `npm test` (full e2e suite) — 33/33 passing locally (was 32)
  • No mock-data-only paths: case exercises the real `aisix` binary, real etcd config propagation, real fetch reverse-call against `/passthrough/anthropic/...`
  • CI green

Refs #151.

…forward (#151)
Third endpoint covered from #151's C7 row. Per gateway docs §4.10,
/passthrough/{provider}/*rest is the lowest-overhead escape hatch
for provider endpoints aisix doesn't natively wrap (OpenAI batches /
files / fine-tuning, Anthropic message-batches, etc.). Prior to
this file the gateway had **zero** e2e coverage on /passthrough —
every customer using batches/files had no regression protection on
the wire.
Case shipped:
Anthropic passthrough: caller POSTs
/passthrough/anthropic/v1/messages/batches with arbitrary body.
Gateway must:
- Strip /passthrough/anthropic prefix and forward the rest
(`/v1/messages/batches`) verbatim to upstream
- Inject Anthropic's auth shape: `x-api-key` + `anthropic-version`
headers (NOT `Authorization: Bearer` — a regression that
forwarded Bearer to Anthropic upstream would 401 in production
but pass against a permissive mock; the explicit auth header
pin is the only line of defence)
- Forward request body byte-for-byte (the entire point of
passthrough is "we don't normalize the body")
- Round-trip the upstream's response shape to the caller
Case held back: OpenAI passthrough surfaced a documented contract
conflict between docs/api-admin.md §4.3 (api_base example
`https://api.openai.com/v1`) and docs/api-proxy.md §4.10 (passthrough
example `/passthrough/openai/v1/batches`) — together they produce
double-/v1 upstream URL. Filed as #164. Same hold-back pattern as
#153 / #154 / #159 / #162.
References:
- Gateway's own /passthrough contract: `docs/api-proxy.md` §4.10
- Anthropic auth headers spec
<https://docs.anthropic.com/en/api/getting-started>
Refs #151
CopilotAI review requested due to automatic review settings May 9, 2026 13:58
@coderabbitai

coderabbitaiBot commented May 9, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@moonming has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 9 minutes and 11 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: ef642733-c724-4207-9d3a-6db4fe2bd416

📥 Commits

Reviewing files that changed from the base of the PR and between b691bf7 and 7a2ed99.

📒 Files selected for processing (1)
  • tests/e2e/src/cases/passthrough-e2e.test.ts

Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

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

CopilotAI 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.

Pull request overview

Adds end-to-end coverage for the gateway’s /passthrough/{provider}/*rest escape hatch, specifically pinning Anthropic passthrough behavior to prevent auth-shape and wire-format regressions.

Changes:

  • Introduces a new e2e test covering POST /passthrough/anthropic/v1/messages/batches.
  • Asserts passthrough path stripping (upstream sees /v1/messages/batches) and verbatim request-body forwarding.
  • Asserts Anthropic-specific auth-shape switching (x-api-key + non-empty anthropic-version) for upstream requests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

expect(typeof testCalls[0]?.headers["anthropic-version"]).toBe("string");
expect(
(testCalls[0]?.headers["anthropic-version"] as string).length,
).toBeGreaterThan(0);
Audit (per CLAUDE.md §8) found two MEDIUM:
MEDIUM M1 (caller-bearer-leak assertion missing despite being one
of the PR's stated motivations):
Initial PR's assertions verified `x-api-key: sk-ant-mock` correct
injection but did NOT pin that the caller's plaintext bearer was
absent on the upstream side — a regression that injected
x-api-key correctly AND forwarded the caller's Bearer would
silently pass.
Audit recommended `expect(authorization).toBeUndefined()`. That
assertion FAILED (gateway today sends `Authorization: Bearer
sk-ant-mock` redundantly alongside x-api-key) — surfacing a
separate issue: NOT a caller-bearer leak (the value is the
gateway's own provider secret, not the caller's plaintext), but
a wire-cleanliness gap vs docs §4.10 which implies x-api-key +
anthropic-version only for Anthropic. Filed separately as #166.
Settled assertion: pin the LEAK concern explicitly — if the
upstream Authorization is present, its value must NOT contain
the caller's plaintext bearer. Catches the regression mode the
PR description claimed to defend against.
MEDIUM M2 (anthropic-version pinned only as "non-empty string"):
Pinned exact value `"2023-06-01"` per Anthropic's documented
current API version
(<https://docs.anthropic.com/en/api/getting-started>). A
regression that injected a malformed-but-non-empty value (e.g.
"v1", "latest", "unspecified") would 400 against real Anthropic
but pass against the permissive mock without this exact pin.
MEDIUM M3-M4 (HTTP method matrix, provider matrix gaps):
Acknowledged scope deferrals; not addressed in this PR. Worth
filing as separate follow-up.
LOW L1-L3: not addressed.
All 33 e2e tests still pass locally.
@moonming
moonming merged commit 971d119 into mainMay 9, 2026
10 of 11 checks passed
@moonming
moonming deleted the test/e2e-c7-passthrough branch May 9, 2026 14:15
moonming added a commit that referenced this pull request May 9, 2026
Closes#157 (test-infra concern, not a product bug).
Issue: the e2e suite's `guardrail-keyword-e2e.test.ts` has flaked
three times on CI in the past 24 hours (#157 first occurrence,
plus reruns required on PR #165 and #167). Failure mode is the
same: `waitConfigPropagation: condition not met within 5s`.
Root cause: vitest is configured with `maxForks: 4` (per
`vitest.config.ts`), so up to 4 test files run in parallel, each
spawning its own `aisix` binary against a SHARED etcd. Each
binary opens watches and writes resources via the admin API
concurrently with the others. Under that load, etcd watch
dispatch latency for the LAST resource in a multi-resource
batch (e.g. a Guardrail rule following Model + ApiKey +
ProviderKey writes) can exceed the 5s budget.
The 5s budget was sized when the suite had ~9 files. The suite
is now 20+ files (#161, #163, #165, #167 added embeddings,
responses, passthrough, rerank, images). The growth in
parallelism load wasn't matched by a budget bump.
Fix: raise the deadline to 10s. This:
- Eliminates the recurring rerun churn on feature PRs
- Preserves the "fail loudly on a genuinely stuck snapshot"
property — 10s is still a generous floor; a real bug where
propagation hangs indefinitely would still fail clearly
- Doesn't change the happy-path latency at all (the helper
polls every 50ms and returns as soon as the condition is
met — bumping the deadline only affects the sad path)
This is a test-infra-only change; no product behavior is affected.
The ≤500ms spec target for in-process propagation is unchanged;
this is purely the CI test harness's wait budget for slow runners
under concurrent load.
If 10s proves insufficient as the suite grows further, the next
escalation would be to reduce `maxForks` from 4 to 2 (slower wall
time, less etcd pressure) — tracked in #157 as a fallback.
moonming added a commit that referenced this pull request May 9, 2026
… (#169)
* test(harness): raise waitConfigPropagation budget 5s → 10s (#157)
Closes#157 (test-infra concern, not a product bug).
Issue: the e2e suite's `guardrail-keyword-e2e.test.ts` has flaked
three times on CI in the past 24 hours (#157 first occurrence,
plus reruns required on PR #165 and #167). Failure mode is the
same: `waitConfigPropagation: condition not met within 5s`.
Root cause: vitest is configured with `maxForks: 4` (per
`vitest.config.ts`), so up to 4 test files run in parallel, each
spawning its own `aisix` binary against a SHARED etcd. Each
binary opens watches and writes resources via the admin API
concurrently with the others. Under that load, etcd watch
dispatch latency for the LAST resource in a multi-resource
batch (e.g. a Guardrail rule following Model + ApiKey +
ProviderKey writes) can exceed the 5s budget.
The 5s budget was sized when the suite had ~9 files. The suite
is now 20+ files (#161, #163, #165, #167 added embeddings,
responses, passthrough, rerank, images). The growth in
parallelism load wasn't matched by a budget bump.
Fix: raise the deadline to 10s. This:
- Eliminates the recurring rerun churn on feature PRs
- Preserves the "fail loudly on a genuinely stuck snapshot"
property — 10s is still a generous floor; a real bug where
propagation hangs indefinitely would still fail clearly
- Doesn't change the happy-path latency at all (the helper
polls every 50ms and returns as soon as the condition is
met — bumping the deadline only affects the sad path)
This is a test-infra-only change; no product behavior is affected.
The ≤500ms spec target for in-process propagation is unchanged;
this is purely the CI test harness's wait budget for slow runners
under concurrent load.
If 10s proves insufficient as the suite grows further, the next
escalation would be to reduce `maxForks` from 4 to 2 (slower wall
time, less etcd pressure) — tracked in #157 as a fallback.
* test(harness): also reduce maxForks 4→2 (#157 fallback escalation)
After the timeout bump landed, #169's own CI run still flaked with
the same `condition not met within 10s` error — meaning etcd watch
dispatch latency genuinely exceeds 10s under maxForks=4 with the
current suite size (20+ files). The audit on #169 had flagged this
as the "product-side hypothesis still open" — confirmed.
Apply the documented fallback from #157: cut maxForks from 4 to 2.
This halves the concurrent-watcher count against the shared etcd
and brings dispatch latency back inside the budget.
Trade-off: wall time grows ~1.5-2× per CI run (locally measured
~14s @ maxForks=4 vs ~30s @ maxForks=2, so faster than expected
because the extra parallelism wasn't fully utilized anyway —
contention dominated). Net for CI is "predictable green" vs
"fast but constantly-rerunning".
The 10s `waitConfigPropagation` budget from the prior commit stays
in place as belt-and-suspenders — even with maxForks=2 the original
5s would still be tight on slow runners.
Combined this PR now does:
1. waitConfigPropagation deadline 5s → 10s (`harness/admin.ts`)
2. maxForks 4 → 2 (`vitest.config.ts`)
The product-side hypothesis ("etcd watch dispatch degrades
non-linearly with concurrent watchers") is now strongly supported
and worth a separate product-side investigation tracked in #157
follow-up.
moonming added a commit that referenced this pull request May 10, 2026
#166) (#209)
## Problem
Per `docs/api-proxy.md` §4.10, Anthropic passthrough was supposed to
inject the documented Anthropic auth shape — `x-api-key` +
`anthropic-version` — and only that. In practice the gateway was
ALSO injecting `Authorization: Bearer <provider-secret>` alongside:
```
x-api-key: sk-ant-mock ← correct per Anthropic spec
anthropic-version: 2023-06-01 ← correct per Anthropic spec
Authorization: Bearer sk-ant-mock ← redundant, not in Anthropic's auth contract
```
Real Anthropic ignores the redundant Bearer today, but:
- Future stricter Anthropic gateways or customer-side middleware
could reject the request based on having unexpected auth headers.
- Operators inspecting upstream traffic captures generate
"is this a leak?" tickets every time the redundant header
surfaces — even though the value is the gateway's own provider
secret, not the caller's plaintext bearer.
Not a credential leak (verified by #165's existing assertion that
the caller's plaintext bearer never reaches the upstream), but a
docs-vs-behavior gap that operators shouldn't have to investigate.
## Fix
In `crates/aisix-proxy/src/passthrough.rs::dispatch`, restructure the
auth-injection branch so each provider gets EXACTLY ONE auth shape:
- openai / gemini / deepseek / unknown providers → `Authorization: Bearer …`
- anthropic → `x-api-key` + `anthropic-version` ONLY
- empty `api_key` → no auth headers at all (was previously: still
injected an empty `x-api-key` for anthropic — also cleaned up)
```rust
if api_key.is_empty() {
// nothing to inject
} else if provider_lower == "anthropic" {
builder = builder.header("x-api-key", &api_key);
builder = builder.header("anthropic-version", "2023-06-01");
} else {
builder = builder.header(header::AUTHORIZATION, format!("Bearer {api_key}"));
}
```
## Tests
**Rust unit (`crates/aisix-proxy/src/passthrough.rs`)** — new
`anthropic_passthrough_does_not_inject_bearer_auth` test:
- Configures Anthropic passthrough with secret `sk-ant-test`
- Posts to `/passthrough/anthropic/v1/messages/batches`
- wiremock subset-match on `x-api-key` + `anthropic-version`
- **Strict assertion**: drains `received_requests` and asserts
`!headers.contains_key("authorization")` — the redundant Bearer
must NOT be on the wire
- Sanity: positive value-pin on `x-api-key: sk-ant-test` and
`anthropic-version: 2023-06-01`
**E2E (`tests/e2e/src/cases/passthrough-e2e.test.ts`)** — tightened
the existing Anthropic case from the lenient
`if (auth !== undefined) expect(auth).not.toContain(CALLER_PLAINTEXT)`
to the strict `expect(headers["authorization"]).toBeUndefined()`.
The lenient version caught caller-plaintext leaks (covered by #165);
the strict version catches the wire-cleanliness regression #166
fixes.
Helpers added: `anthropic_model()` and
`anthropic_provider_key_entry()` for the Anthropic-shaped fixtures
(parallel to the existing `openai_model` / `provider_key_entry`).
## Verification
- `cargo test -p aisix-proxy --lib`: **152/152 passing** (incl. new test)
- `cargo clippy -p aisix-proxy --lib --tests -- -D warnings`: clean
- `cargo fmt --check`: clean
- `pnpm tsc --noEmit` (e2e): clean
## References
- Issue: #166
- `docs/api-proxy.md` §4.10 (passthrough contract)
- Anthropic auth spec: <https://docs.anthropic.com/en/api/getting-started>
Sign up for freeto 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.

2 participants

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

test(e2e): C7 /passthrough/anthropic auth-shape switching + verbatim forward - #165

Merged
moonming merged 2 commits into
mainfrom
test/e2e-c7-passthrough
May 9, 2026
Merged

test(e2e): C7 /passthrough/anthropic auth-shape switching + verbatim forward#165
moonming merged 2 commits into
mainfrom
test/e2e-c7-passthrough

Conversation

@moonming

@moonmingmoonming commented May 9, 2026

Copy link
Copy Markdown
Member

Summary

Third endpoint covered from #151's C7 row. Per gateway docs §4.10, `/passthrough/{provider}/*rest` is the lowest-overhead escape hatch for provider endpoints aisix doesn't natively wrap (OpenAI batches / files / fine-tuning, Anthropic message-batches). Prior to this PR the gateway had zero e2e coverage on `/passthrough`.

One of two originally-planned cases ships here. The other surfaced a docs/behavior conflict and is held back as #164.

What's pinned

CaseUser journeyAsserts
Anthropic passthroughPOST `/passthrough/anthropic/v1/messages/batches` with arbitrary bodyCaller sees upstream's response back; upstream got POST at `/v1/messages/batches` (path verbatim after `/passthrough/anthropic` stripped); auth-shape switching: `x-api-key: sk-ant-mock` (NOT caller's bearer) + `anthropic-version: 2023-06-01` (Anthropic's documented current version); upstream Authorization (if present) does not contain caller's plaintext bearer (no credential leak); body forwarded byte-for-byte

Held back as separate issues

🐛 #164 — `/passthrough/openai/*` doubles `/v1` prefix when api_base ends with `/v1` (contradicts published docs). `docs/api-admin.md` §4.3 publishes `api_base: "https://api.openai.com/v1"\`; `docs/api-proxy.md` §4.10 publishes the call as `/passthrough/openai/v1/batches`. Together: `/v1/v1/batches` upstream. OpenAI passthrough test will return once reconciled.

🐛 #166 — `/passthrough/anthropic` injects redundant `Authorization: Bearer` alongside x-api-key. NOT a credential leak (Bearer value is gateway's own provider secret, not caller's plaintext) but a wire-cleanliness gap vs docs §4.10 implication of x-api-key + anthropic-version only for Anthropic. Test pins the leak concern explicitly; the strict "Authorization absent" assertion is the verification once #166 is resolved.

Why this matters

Three real-world failure modes that prior coverage couldn't catch:

  • Auth-shape regression — a regression that forwarded `Authorization: Bearer` to Anthropic upstream as the only auth header would 401 in production.
  • Caller's bearer leak — the test verifies that even when Authorization IS present (per bug: /passthrough/anthropic injects redundant Authorization: Bearer <provider-secret> alongside x-api-key #166's wire-cleanliness gap), it does NOT contain the caller's plaintext bearer.
  • Body normalization regression — passthrough's whole point is "we don't normalize". A regression that JSON-round-tripped or schema-validated bodies on the way through would silently drop unknown fields, breaking batches/files use cases that depend on free-form bodies.

Source-blind discipline

Every assertion derives from external contracts:

No internal Rust paths or struct field names referenced.

Independent audit

Per CLAUDE.md §8, an independent audit agent reviewed the initial commit (4ffd636). Resolution log:

FindingSeverityResolution
Caller-bearer-leak assertion missing despite being a stated motivationMEDIUMTightened: upstream Authorization (if present) must not contain caller's plaintext bearer (7a2ed99)
`anthropic-version` pinned only as "non-empty string" — would pass on malformed values like `"v1"`MEDIUMPinned exact value `"2023-06-01"` per Anthropic docs (7a2ed99)
HTTP method matrix coverage gap (route is `ANY`; only POST tested)MEDIUMAcknowledged scope deferral
Provider matrix coverage gap (only anthropic; gemini/deepseek not tested)MEDIUMAcknowledged scope deferral; OpenAI held back as #164
Body verbatim assertion is string-equalityMEDIUMDeliberate strict contract per docs §4.10 — no change
L1-L3 informationalLOWNot addressed

The audit's recommended strict assertion (`Authorization === undefined`) revealed a separate gateway-side issue (#166): NOT a caller-bearer leak, but a redundant Bearer injection alongside x-api-key. The shipped assertion pins the leak concern explicitly while #166 tracks the wire-cleanliness gap.

Test plan

  • `npm test` (full e2e suite) — 33/33 passing locally (was 32)
  • No mock-data-only paths: case exercises the real `aisix` binary, real etcd config propagation, real fetch reverse-call against `/passthrough/anthropic/...`
  • CI green

Refs #151.

…forward (#151)
Third endpoint covered from #151's C7 row. Per gateway docs §4.10,
/passthrough/{provider}/*rest is the lowest-overhead escape hatch
for provider endpoints aisix doesn't natively wrap (OpenAI batches /
files / fine-tuning, Anthropic message-batches, etc.). Prior to
this file the gateway had **zero** e2e coverage on /passthrough —
every customer using batches/files had no regression protection on
the wire.
Case shipped:
Anthropic passthrough: caller POSTs
/passthrough/anthropic/v1/messages/batches with arbitrary body.
Gateway must:
- Strip /passthrough/anthropic prefix and forward the rest
(`/v1/messages/batches`) verbatim to upstream
- Inject Anthropic's auth shape: `x-api-key` + `anthropic-version`
headers (NOT `Authorization: Bearer` — a regression that
forwarded Bearer to Anthropic upstream would 401 in production
but pass against a permissive mock; the explicit auth header
pin is the only line of defence)
- Forward request body byte-for-byte (the entire point of
passthrough is "we don't normalize the body")
- Round-trip the upstream's response shape to the caller
Case held back: OpenAI passthrough surfaced a documented contract
conflict between docs/api-admin.md §4.3 (api_base example
`https://api.openai.com/v1`) and docs/api-proxy.md §4.10 (passthrough
example `/passthrough/openai/v1/batches`) — together they produce
double-/v1 upstream URL. Filed as #164. Same hold-back pattern as
#153 / #154 / #159 / #162.
References:
- Gateway's own /passthrough contract: `docs/api-proxy.md` §4.10
- Anthropic auth headers spec
<https://docs.anthropic.com/en/api/getting-started>
Refs #151
CopilotAI review requested due to automatic review settings May 9, 2026 13:58
@coderabbitai

coderabbitaiBot commented May 9, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@moonming has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 9 minutes and 11 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: ef642733-c724-4207-9d3a-6db4fe2bd416

📥 Commits

Reviewing files that changed from the base of the PR and between b691bf7 and 7a2ed99.

📒 Files selected for processing (1)
  • tests/e2e/src/cases/passthrough-e2e.test.ts

Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

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

CopilotAI 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.

Pull request overview

Adds end-to-end coverage for the gateway’s /passthrough/{provider}/*rest escape hatch, specifically pinning Anthropic passthrough behavior to prevent auth-shape and wire-format regressions.

Changes:

  • Introduces a new e2e test covering POST /passthrough/anthropic/v1/messages/batches.
  • Asserts passthrough path stripping (upstream sees /v1/messages/batches) and verbatim request-body forwarding.
  • Asserts Anthropic-specific auth-shape switching (x-api-key + non-empty anthropic-version) for upstream requests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

expect(typeof testCalls[0]?.headers["anthropic-version"]).toBe("string");
expect(
(testCalls[0]?.headers["anthropic-version"] as string).length,
).toBeGreaterThan(0);
Audit (per CLAUDE.md §8) found two MEDIUM:
MEDIUM M1 (caller-bearer-leak assertion missing despite being one
of the PR's stated motivations):
Initial PR's assertions verified `x-api-key: sk-ant-mock` correct
injection but did NOT pin that the caller's plaintext bearer was
absent on the upstream side — a regression that injected
x-api-key correctly AND forwarded the caller's Bearer would
silently pass.
Audit recommended `expect(authorization).toBeUndefined()`. That
assertion FAILED (gateway today sends `Authorization: Bearer
sk-ant-mock` redundantly alongside x-api-key) — surfacing a
separate issue: NOT a caller-bearer leak (the value is the
gateway's own provider secret, not the caller's plaintext), but
a wire-cleanliness gap vs docs §4.10 which implies x-api-key +
anthropic-version only for Anthropic. Filed separately as #166.
Settled assertion: pin the LEAK concern explicitly — if the
upstream Authorization is present, its value must NOT contain
the caller's plaintext bearer. Catches the regression mode the
PR description claimed to defend against.
MEDIUM M2 (anthropic-version pinned only as "non-empty string"):
Pinned exact value `"2023-06-01"` per Anthropic's documented
current API version
(<https://docs.anthropic.com/en/api/getting-started>). A
regression that injected a malformed-but-non-empty value (e.g.
"v1", "latest", "unspecified") would 400 against real Anthropic
but pass against the permissive mock without this exact pin.
MEDIUM M3-M4 (HTTP method matrix, provider matrix gaps):
Acknowledged scope deferrals; not addressed in this PR. Worth
filing as separate follow-up.
LOW L1-L3: not addressed.
All 33 e2e tests still pass locally.
@moonming
moonming merged commit 971d119 into mainMay 9, 2026
10 of 11 checks passed
@moonming
moonming deleted the test/e2e-c7-passthrough branch May 9, 2026 14:15
moonming added a commit that referenced this pull request May 9, 2026
Closes#157 (test-infra concern, not a product bug).
Issue: the e2e suite's `guardrail-keyword-e2e.test.ts` has flaked
three times on CI in the past 24 hours (#157 first occurrence,
plus reruns required on PR #165 and #167). Failure mode is the
same: `waitConfigPropagation: condition not met within 5s`.
Root cause: vitest is configured with `maxForks: 4` (per
`vitest.config.ts`), so up to 4 test files run in parallel, each
spawning its own `aisix` binary against a SHARED etcd. Each
binary opens watches and writes resources via the admin API
concurrently with the others. Under that load, etcd watch
dispatch latency for the LAST resource in a multi-resource
batch (e.g. a Guardrail rule following Model + ApiKey +
ProviderKey writes) can exceed the 5s budget.
The 5s budget was sized when the suite had ~9 files. The suite
is now 20+ files (#161, #163, #165, #167 added embeddings,
responses, passthrough, rerank, images). The growth in
parallelism load wasn't matched by a budget bump.
Fix: raise the deadline to 10s. This:
- Eliminates the recurring rerun churn on feature PRs
- Preserves the "fail loudly on a genuinely stuck snapshot"
property — 10s is still a generous floor; a real bug where
propagation hangs indefinitely would still fail clearly
- Doesn't change the happy-path latency at all (the helper
polls every 50ms and returns as soon as the condition is
met — bumping the deadline only affects the sad path)
This is a test-infra-only change; no product behavior is affected.
The ≤500ms spec target for in-process propagation is unchanged;
this is purely the CI test harness's wait budget for slow runners
under concurrent load.
If 10s proves insufficient as the suite grows further, the next
escalation would be to reduce `maxForks` from 4 to 2 (slower wall
time, less etcd pressure) — tracked in #157 as a fallback.
moonming added a commit that referenced this pull request May 9, 2026
… (#169)
* test(harness): raise waitConfigPropagation budget 5s → 10s (#157)
Closes#157 (test-infra concern, not a product bug).
Issue: the e2e suite's `guardrail-keyword-e2e.test.ts` has flaked
three times on CI in the past 24 hours (#157 first occurrence,
plus reruns required on PR #165 and #167). Failure mode is the
same: `waitConfigPropagation: condition not met within 5s`.
Root cause: vitest is configured with `maxForks: 4` (per
`vitest.config.ts`), so up to 4 test files run in parallel, each
spawning its own `aisix` binary against a SHARED etcd. Each
binary opens watches and writes resources via the admin API
concurrently with the others. Under that load, etcd watch
dispatch latency for the LAST resource in a multi-resource
batch (e.g. a Guardrail rule following Model + ApiKey +
ProviderKey writes) can exceed the 5s budget.
The 5s budget was sized when the suite had ~9 files. The suite
is now 20+ files (#161, #163, #165, #167 added embeddings,
responses, passthrough, rerank, images). The growth in
parallelism load wasn't matched by a budget bump.
Fix: raise the deadline to 10s. This:
- Eliminates the recurring rerun churn on feature PRs
- Preserves the "fail loudly on a genuinely stuck snapshot"
property — 10s is still a generous floor; a real bug where
propagation hangs indefinitely would still fail clearly
- Doesn't change the happy-path latency at all (the helper
polls every 50ms and returns as soon as the condition is
met — bumping the deadline only affects the sad path)
This is a test-infra-only change; no product behavior is affected.
The ≤500ms spec target for in-process propagation is unchanged;
this is purely the CI test harness's wait budget for slow runners
under concurrent load.
If 10s proves insufficient as the suite grows further, the next
escalation would be to reduce `maxForks` from 4 to 2 (slower wall
time, less etcd pressure) — tracked in #157 as a fallback.
* test(harness): also reduce maxForks 4→2 (#157 fallback escalation)
After the timeout bump landed, #169's own CI run still flaked with
the same `condition not met within 10s` error — meaning etcd watch
dispatch latency genuinely exceeds 10s under maxForks=4 with the
current suite size (20+ files). The audit on #169 had flagged this
as the "product-side hypothesis still open" — confirmed.
Apply the documented fallback from #157: cut maxForks from 4 to 2.
This halves the concurrent-watcher count against the shared etcd
and brings dispatch latency back inside the budget.
Trade-off: wall time grows ~1.5-2× per CI run (locally measured
~14s @ maxForks=4 vs ~30s @ maxForks=2, so faster than expected
because the extra parallelism wasn't fully utilized anyway —
contention dominated). Net for CI is "predictable green" vs
"fast but constantly-rerunning".
The 10s `waitConfigPropagation` budget from the prior commit stays
in place as belt-and-suspenders — even with maxForks=2 the original
5s would still be tight on slow runners.
Combined this PR now does:
1. waitConfigPropagation deadline 5s → 10s (`harness/admin.ts`)
2. maxForks 4 → 2 (`vitest.config.ts`)
The product-side hypothesis ("etcd watch dispatch degrades
non-linearly with concurrent watchers") is now strongly supported
and worth a separate product-side investigation tracked in #157
follow-up.
moonming added a commit that referenced this pull request May 10, 2026
#166) (#209)
## Problem
Per `docs/api-proxy.md` §4.10, Anthropic passthrough was supposed to
inject the documented Anthropic auth shape — `x-api-key` +
`anthropic-version` — and only that. In practice the gateway was
ALSO injecting `Authorization: Bearer <provider-secret>` alongside:
```
x-api-key: sk-ant-mock ← correct per Anthropic spec
anthropic-version: 2023-06-01 ← correct per Anthropic spec
Authorization: Bearer sk-ant-mock ← redundant, not in Anthropic's auth contract
```
Real Anthropic ignores the redundant Bearer today, but:
- Future stricter Anthropic gateways or customer-side middleware
could reject the request based on having unexpected auth headers.
- Operators inspecting upstream traffic captures generate
"is this a leak?" tickets every time the redundant header
surfaces — even though the value is the gateway's own provider
secret, not the caller's plaintext bearer.
Not a credential leak (verified by #165's existing assertion that
the caller's plaintext bearer never reaches the upstream), but a
docs-vs-behavior gap that operators shouldn't have to investigate.
## Fix
In `crates/aisix-proxy/src/passthrough.rs::dispatch`, restructure the
auth-injection branch so each provider gets EXACTLY ONE auth shape:
- openai / gemini / deepseek / unknown providers → `Authorization: Bearer …`
- anthropic → `x-api-key` + `anthropic-version` ONLY
- empty `api_key` → no auth headers at all (was previously: still
injected an empty `x-api-key` for anthropic — also cleaned up)
```rust
if api_key.is_empty() {
// nothing to inject
} else if provider_lower == "anthropic" {
builder = builder.header("x-api-key", &api_key);
builder = builder.header("anthropic-version", "2023-06-01");
} else {
builder = builder.header(header::AUTHORIZATION, format!("Bearer {api_key}"));
}
```
## Tests
**Rust unit (`crates/aisix-proxy/src/passthrough.rs`)** — new
`anthropic_passthrough_does_not_inject_bearer_auth` test:
- Configures Anthropic passthrough with secret `sk-ant-test`
- Posts to `/passthrough/anthropic/v1/messages/batches`
- wiremock subset-match on `x-api-key` + `anthropic-version`
- **Strict assertion**: drains `received_requests` and asserts
`!headers.contains_key("authorization")` — the redundant Bearer
must NOT be on the wire
- Sanity: positive value-pin on `x-api-key: sk-ant-test` and
`anthropic-version: 2023-06-01`
**E2E (`tests/e2e/src/cases/passthrough-e2e.test.ts`)** — tightened
the existing Anthropic case from the lenient
`if (auth !== undefined) expect(auth).not.toContain(CALLER_PLAINTEXT)`
to the strict `expect(headers["authorization"]).toBeUndefined()`.
The lenient version caught caller-plaintext leaks (covered by #165);
the strict version catches the wire-cleanliness regression #166
fixes.
Helpers added: `anthropic_model()` and
`anthropic_provider_key_entry()` for the Anthropic-shaped fixtures
(parallel to the existing `openai_model` / `provider_key_entry`).
## Verification
- `cargo test -p aisix-proxy --lib`: **152/152 passing** (incl. new test)
- `cargo clippy -p aisix-proxy --lib --tests -- -D warnings`: clean
- `cargo fmt --check`: clean
- `pnpm tsc --noEmit` (e2e): clean
## References
- Issue: #166
- `docs/api-proxy.md` §4.10 (passthrough contract)
- Anthropic auth spec: <https://docs.anthropic.com/en/api/getting-started>
Sign up for freeto 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.

2 participants

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

test(e2e): C7 /passthrough/anthropic auth-shape switching + verbatim forward - #165

Merged
moonming merged 2 commits into
mainfrom
test/e2e-c7-passthrough
May 9, 2026
Merged

test(e2e): C7 /passthrough/anthropic auth-shape switching + verbatim forward#165
moonming merged 2 commits into
mainfrom
test/e2e-c7-passthrough

Conversation

@moonming

@moonmingmoonming commented May 9, 2026

Copy link
Copy Markdown
Member

Summary

Third endpoint covered from #151's C7 row. Per gateway docs §4.10, `/passthrough/{provider}/*rest` is the lowest-overhead escape hatch for provider endpoints aisix doesn't natively wrap (OpenAI batches / files / fine-tuning, Anthropic message-batches). Prior to this PR the gateway had zero e2e coverage on `/passthrough`.

One of two originally-planned cases ships here. The other surfaced a docs/behavior conflict and is held back as #164.

What's pinned

CaseUser journeyAsserts
Anthropic passthroughPOST `/passthrough/anthropic/v1/messages/batches` with arbitrary bodyCaller sees upstream's response back; upstream got POST at `/v1/messages/batches` (path verbatim after `/passthrough/anthropic` stripped); auth-shape switching: `x-api-key: sk-ant-mock` (NOT caller's bearer) + `anthropic-version: 2023-06-01` (Anthropic's documented current version); upstream Authorization (if present) does not contain caller's plaintext bearer (no credential leak); body forwarded byte-for-byte

Held back as separate issues

🐛 #164 — `/passthrough/openai/*` doubles `/v1` prefix when api_base ends with `/v1` (contradicts published docs). `docs/api-admin.md` §4.3 publishes `api_base: "https://api.openai.com/v1"\`; `docs/api-proxy.md` §4.10 publishes the call as `/passthrough/openai/v1/batches`. Together: `/v1/v1/batches` upstream. OpenAI passthrough test will return once reconciled.

🐛 #166 — `/passthrough/anthropic` injects redundant `Authorization: Bearer` alongside x-api-key. NOT a credential leak (Bearer value is gateway's own provider secret, not caller's plaintext) but a wire-cleanliness gap vs docs §4.10 implication of x-api-key + anthropic-version only for Anthropic. Test pins the leak concern explicitly; the strict "Authorization absent" assertion is the verification once #166 is resolved.

Why this matters

Three real-world failure modes that prior coverage couldn't catch:

  • Auth-shape regression — a regression that forwarded `Authorization: Bearer` to Anthropic upstream as the only auth header would 401 in production.
  • Caller's bearer leak — the test verifies that even when Authorization IS present (per bug: /passthrough/anthropic injects redundant Authorization: Bearer <provider-secret> alongside x-api-key #166's wire-cleanliness gap), it does NOT contain the caller's plaintext bearer.
  • Body normalization regression — passthrough's whole point is "we don't normalize". A regression that JSON-round-tripped or schema-validated bodies on the way through would silently drop unknown fields, breaking batches/files use cases that depend on free-form bodies.

Source-blind discipline

Every assertion derives from external contracts:

No internal Rust paths or struct field names referenced.

Independent audit

Per CLAUDE.md §8, an independent audit agent reviewed the initial commit (4ffd636). Resolution log:

FindingSeverityResolution
Caller-bearer-leak assertion missing despite being a stated motivationMEDIUMTightened: upstream Authorization (if present) must not contain caller's plaintext bearer (7a2ed99)
`anthropic-version` pinned only as "non-empty string" — would pass on malformed values like `"v1"`MEDIUMPinned exact value `"2023-06-01"` per Anthropic docs (7a2ed99)
HTTP method matrix coverage gap (route is `ANY`; only POST tested)MEDIUMAcknowledged scope deferral
Provider matrix coverage gap (only anthropic; gemini/deepseek not tested)MEDIUMAcknowledged scope deferral; OpenAI held back as #164
Body verbatim assertion is string-equalityMEDIUMDeliberate strict contract per docs §4.10 — no change
L1-L3 informationalLOWNot addressed

The audit's recommended strict assertion (`Authorization === undefined`) revealed a separate gateway-side issue (#166): NOT a caller-bearer leak, but a redundant Bearer injection alongside x-api-key. The shipped assertion pins the leak concern explicitly while #166 tracks the wire-cleanliness gap.

Test plan

  • `npm test` (full e2e suite) — 33/33 passing locally (was 32)
  • No mock-data-only paths: case exercises the real `aisix` binary, real etcd config propagation, real fetch reverse-call against `/passthrough/anthropic/...`
  • CI green

Refs #151.

…forward (#151)
Third endpoint covered from #151's C7 row. Per gateway docs §4.10,
/passthrough/{provider}/*rest is the lowest-overhead escape hatch
for provider endpoints aisix doesn't natively wrap (OpenAI batches /
files / fine-tuning, Anthropic message-batches, etc.). Prior to
this file the gateway had **zero** e2e coverage on /passthrough —
every customer using batches/files had no regression protection on
the wire.
Case shipped:
Anthropic passthrough: caller POSTs
/passthrough/anthropic/v1/messages/batches with arbitrary body.
Gateway must:
- Strip /passthrough/anthropic prefix and forward the rest
(`/v1/messages/batches`) verbatim to upstream
- Inject Anthropic's auth shape: `x-api-key` + `anthropic-version`
headers (NOT `Authorization: Bearer` — a regression that
forwarded Bearer to Anthropic upstream would 401 in production
but pass against a permissive mock; the explicit auth header
pin is the only line of defence)
- Forward request body byte-for-byte (the entire point of
passthrough is "we don't normalize the body")
- Round-trip the upstream's response shape to the caller
Case held back: OpenAI passthrough surfaced a documented contract
conflict between docs/api-admin.md §4.3 (api_base example
`https://api.openai.com/v1`) and docs/api-proxy.md §4.10 (passthrough
example `/passthrough/openai/v1/batches`) — together they produce
double-/v1 upstream URL. Filed as #164. Same hold-back pattern as
#153 / #154 / #159 / #162.
References:
- Gateway's own /passthrough contract: `docs/api-proxy.md` §4.10
- Anthropic auth headers spec
<https://docs.anthropic.com/en/api/getting-started>
Refs #151
CopilotAI review requested due to automatic review settings May 9, 2026 13:58
@coderabbitai

coderabbitaiBot commented May 9, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@moonming has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 9 minutes and 11 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: ef642733-c724-4207-9d3a-6db4fe2bd416

📥 Commits

Reviewing files that changed from the base of the PR and between b691bf7 and 7a2ed99.

📒 Files selected for processing (1)
  • tests/e2e/src/cases/passthrough-e2e.test.ts

Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

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

CopilotAI 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.

Pull request overview

Adds end-to-end coverage for the gateway’s /passthrough/{provider}/*rest escape hatch, specifically pinning Anthropic passthrough behavior to prevent auth-shape and wire-format regressions.

Changes:

  • Introduces a new e2e test covering POST /passthrough/anthropic/v1/messages/batches.
  • Asserts passthrough path stripping (upstream sees /v1/messages/batches) and verbatim request-body forwarding.
  • Asserts Anthropic-specific auth-shape switching (x-api-key + non-empty anthropic-version) for upstream requests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

expect(typeof testCalls[0]?.headers["anthropic-version"]).toBe("string");
expect(
(testCalls[0]?.headers["anthropic-version"] as string).length,
).toBeGreaterThan(0);
Audit (per CLAUDE.md §8) found two MEDIUM:
MEDIUM M1 (caller-bearer-leak assertion missing despite being one
of the PR's stated motivations):
Initial PR's assertions verified `x-api-key: sk-ant-mock` correct
injection but did NOT pin that the caller's plaintext bearer was
absent on the upstream side — a regression that injected
x-api-key correctly AND forwarded the caller's Bearer would
silently pass.
Audit recommended `expect(authorization).toBeUndefined()`. That
assertion FAILED (gateway today sends `Authorization: Bearer
sk-ant-mock` redundantly alongside x-api-key) — surfacing a
separate issue: NOT a caller-bearer leak (the value is the
gateway's own provider secret, not the caller's plaintext), but
a wire-cleanliness gap vs docs §4.10 which implies x-api-key +
anthropic-version only for Anthropic. Filed separately as #166.
Settled assertion: pin the LEAK concern explicitly — if the
upstream Authorization is present, its value must NOT contain
the caller's plaintext bearer. Catches the regression mode the
PR description claimed to defend against.
MEDIUM M2 (anthropic-version pinned only as "non-empty string"):
Pinned exact value `"2023-06-01"` per Anthropic's documented
current API version
(<https://docs.anthropic.com/en/api/getting-started>). A
regression that injected a malformed-but-non-empty value (e.g.
"v1", "latest", "unspecified") would 400 against real Anthropic
but pass against the permissive mock without this exact pin.
MEDIUM M3-M4 (HTTP method matrix, provider matrix gaps):
Acknowledged scope deferrals; not addressed in this PR. Worth
filing as separate follow-up.
LOW L1-L3: not addressed.
All 33 e2e tests still pass locally.
@moonming
moonming merged commit 971d119 into mainMay 9, 2026
10 of 11 checks passed
@moonming
moonming deleted the test/e2e-c7-passthrough branch May 9, 2026 14:15
moonming added a commit that referenced this pull request May 9, 2026
Closes#157 (test-infra concern, not a product bug).
Issue: the e2e suite's `guardrail-keyword-e2e.test.ts` has flaked
three times on CI in the past 24 hours (#157 first occurrence,
plus reruns required on PR #165 and #167). Failure mode is the
same: `waitConfigPropagation: condition not met within 5s`.
Root cause: vitest is configured with `maxForks: 4` (per
`vitest.config.ts`), so up to 4 test files run in parallel, each
spawning its own `aisix` binary against a SHARED etcd. Each
binary opens watches and writes resources via the admin API
concurrently with the others. Under that load, etcd watch
dispatch latency for the LAST resource in a multi-resource
batch (e.g. a Guardrail rule following Model + ApiKey +
ProviderKey writes) can exceed the 5s budget.
The 5s budget was sized when the suite had ~9 files. The suite
is now 20+ files (#161, #163, #165, #167 added embeddings,
responses, passthrough, rerank, images). The growth in
parallelism load wasn't matched by a budget bump.
Fix: raise the deadline to 10s. This:
- Eliminates the recurring rerun churn on feature PRs
- Preserves the "fail loudly on a genuinely stuck snapshot"
property — 10s is still a generous floor; a real bug where
propagation hangs indefinitely would still fail clearly
- Doesn't change the happy-path latency at all (the helper
polls every 50ms and returns as soon as the condition is
met — bumping the deadline only affects the sad path)
This is a test-infra-only change; no product behavior is affected.
The ≤500ms spec target for in-process propagation is unchanged;
this is purely the CI test harness's wait budget for slow runners
under concurrent load.
If 10s proves insufficient as the suite grows further, the next
escalation would be to reduce `maxForks` from 4 to 2 (slower wall
time, less etcd pressure) — tracked in #157 as a fallback.
moonming added a commit that referenced this pull request May 9, 2026
… (#169)
* test(harness): raise waitConfigPropagation budget 5s → 10s (#157)
Closes#157 (test-infra concern, not a product bug).
Issue: the e2e suite's `guardrail-keyword-e2e.test.ts` has flaked
three times on CI in the past 24 hours (#157 first occurrence,
plus reruns required on PR #165 and #167). Failure mode is the
same: `waitConfigPropagation: condition not met within 5s`.
Root cause: vitest is configured with `maxForks: 4` (per
`vitest.config.ts`), so up to 4 test files run in parallel, each
spawning its own `aisix` binary against a SHARED etcd. Each
binary opens watches and writes resources via the admin API
concurrently with the others. Under that load, etcd watch
dispatch latency for the LAST resource in a multi-resource
batch (e.g. a Guardrail rule following Model + ApiKey +
ProviderKey writes) can exceed the 5s budget.
The 5s budget was sized when the suite had ~9 files. The suite
is now 20+ files (#161, #163, #165, #167 added embeddings,
responses, passthrough, rerank, images). The growth in
parallelism load wasn't matched by a budget bump.
Fix: raise the deadline to 10s. This:
- Eliminates the recurring rerun churn on feature PRs
- Preserves the "fail loudly on a genuinely stuck snapshot"
property — 10s is still a generous floor; a real bug where
propagation hangs indefinitely would still fail clearly
- Doesn't change the happy-path latency at all (the helper
polls every 50ms and returns as soon as the condition is
met — bumping the deadline only affects the sad path)
This is a test-infra-only change; no product behavior is affected.
The ≤500ms spec target for in-process propagation is unchanged;
this is purely the CI test harness's wait budget for slow runners
under concurrent load.
If 10s proves insufficient as the suite grows further, the next
escalation would be to reduce `maxForks` from 4 to 2 (slower wall
time, less etcd pressure) — tracked in #157 as a fallback.
* test(harness): also reduce maxForks 4→2 (#157 fallback escalation)
After the timeout bump landed, #169's own CI run still flaked with
the same `condition not met within 10s` error — meaning etcd watch
dispatch latency genuinely exceeds 10s under maxForks=4 with the
current suite size (20+ files). The audit on #169 had flagged this
as the "product-side hypothesis still open" — confirmed.
Apply the documented fallback from #157: cut maxForks from 4 to 2.
This halves the concurrent-watcher count against the shared etcd
and brings dispatch latency back inside the budget.
Trade-off: wall time grows ~1.5-2× per CI run (locally measured
~14s @ maxForks=4 vs ~30s @ maxForks=2, so faster than expected
because the extra parallelism wasn't fully utilized anyway —
contention dominated). Net for CI is "predictable green" vs
"fast but constantly-rerunning".
The 10s `waitConfigPropagation` budget from the prior commit stays
in place as belt-and-suspenders — even with maxForks=2 the original
5s would still be tight on slow runners.
Combined this PR now does:
1. waitConfigPropagation deadline 5s → 10s (`harness/admin.ts`)
2. maxForks 4 → 2 (`vitest.config.ts`)
The product-side hypothesis ("etcd watch dispatch degrades
non-linearly with concurrent watchers") is now strongly supported
and worth a separate product-side investigation tracked in #157
follow-up.
moonming added a commit that referenced this pull request May 10, 2026
#166) (#209)
## Problem
Per `docs/api-proxy.md` §4.10, Anthropic passthrough was supposed to
inject the documented Anthropic auth shape — `x-api-key` +
`anthropic-version` — and only that. In practice the gateway was
ALSO injecting `Authorization: Bearer <provider-secret>` alongside:
```
x-api-key: sk-ant-mock ← correct per Anthropic spec
anthropic-version: 2023-06-01 ← correct per Anthropic spec
Authorization: Bearer sk-ant-mock ← redundant, not in Anthropic's auth contract
```
Real Anthropic ignores the redundant Bearer today, but:
- Future stricter Anthropic gateways or customer-side middleware
could reject the request based on having unexpected auth headers.
- Operators inspecting upstream traffic captures generate
"is this a leak?" tickets every time the redundant header
surfaces — even though the value is the gateway's own provider
secret, not the caller's plaintext bearer.
Not a credential leak (verified by #165's existing assertion that
the caller's plaintext bearer never reaches the upstream), but a
docs-vs-behavior gap that operators shouldn't have to investigate.
## Fix
In `crates/aisix-proxy/src/passthrough.rs::dispatch`, restructure the
auth-injection branch so each provider gets EXACTLY ONE auth shape:
- openai / gemini / deepseek / unknown providers → `Authorization: Bearer …`
- anthropic → `x-api-key` + `anthropic-version` ONLY
- empty `api_key` → no auth headers at all (was previously: still
injected an empty `x-api-key` for anthropic — also cleaned up)
```rust
if api_key.is_empty() {
// nothing to inject
} else if provider_lower == "anthropic" {
builder = builder.header("x-api-key", &api_key);
builder = builder.header("anthropic-version", "2023-06-01");
} else {
builder = builder.header(header::AUTHORIZATION, format!("Bearer {api_key}"));
}
```
## Tests
**Rust unit (`crates/aisix-proxy/src/passthrough.rs`)** — new
`anthropic_passthrough_does_not_inject_bearer_auth` test:
- Configures Anthropic passthrough with secret `sk-ant-test`
- Posts to `/passthrough/anthropic/v1/messages/batches`
- wiremock subset-match on `x-api-key` + `anthropic-version`
- **Strict assertion**: drains `received_requests` and asserts
`!headers.contains_key("authorization")` — the redundant Bearer
must NOT be on the wire
- Sanity: positive value-pin on `x-api-key: sk-ant-test` and
`anthropic-version: 2023-06-01`
**E2E (`tests/e2e/src/cases/passthrough-e2e.test.ts`)** — tightened
the existing Anthropic case from the lenient
`if (auth !== undefined) expect(auth).not.toContain(CALLER_PLAINTEXT)`
to the strict `expect(headers["authorization"]).toBeUndefined()`.
The lenient version caught caller-plaintext leaks (covered by #165);
the strict version catches the wire-cleanliness regression #166
fixes.
Helpers added: `anthropic_model()` and
`anthropic_provider_key_entry()` for the Anthropic-shaped fixtures
(parallel to the existing `openai_model` / `provider_key_entry`).
## Verification
- `cargo test -p aisix-proxy --lib`: **152/152 passing** (incl. new test)
- `cargo clippy -p aisix-proxy --lib --tests -- -D warnings`: clean
- `cargo fmt --check`: clean
- `pnpm tsc --noEmit` (e2e): clean
## References
- Issue: #166
- `docs/api-proxy.md` §4.10 (passthrough contract)
- Anthropic auth spec: <https://docs.anthropic.com/en/api/getting-started>
Sign up for freeto 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.

2 participants

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

test(e2e): C7 /passthrough/anthropic auth-shape switching + verbatim forward - #165

Merged
moonming merged 2 commits into
mainfrom
test/e2e-c7-passthrough
May 9, 2026
Merged

test(e2e): C7 /passthrough/anthropic auth-shape switching + verbatim forward#165
moonming merged 2 commits into
mainfrom
test/e2e-c7-passthrough

Conversation

@moonming

@moonmingmoonming commented May 9, 2026

Copy link
Copy Markdown
Member

Summary

Third endpoint covered from #151's C7 row. Per gateway docs §4.10, `/passthrough/{provider}/*rest` is the lowest-overhead escape hatch for provider endpoints aisix doesn't natively wrap (OpenAI batches / files / fine-tuning, Anthropic message-batches). Prior to this PR the gateway had zero e2e coverage on `/passthrough`.

One of two originally-planned cases ships here. The other surfaced a docs/behavior conflict and is held back as #164.

What's pinned

CaseUser journeyAsserts
Anthropic passthroughPOST `/passthrough/anthropic/v1/messages/batches` with arbitrary bodyCaller sees upstream's response back; upstream got POST at `/v1/messages/batches` (path verbatim after `/passthrough/anthropic` stripped); auth-shape switching: `x-api-key: sk-ant-mock` (NOT caller's bearer) + `anthropic-version: 2023-06-01` (Anthropic's documented current version); upstream Authorization (if present) does not contain caller's plaintext bearer (no credential leak); body forwarded byte-for-byte

Held back as separate issues

🐛 #164 — `/passthrough/openai/*` doubles `/v1` prefix when api_base ends with `/v1` (contradicts published docs). `docs/api-admin.md` §4.3 publishes `api_base: "https://api.openai.com/v1"\`; `docs/api-proxy.md` §4.10 publishes the call as `/passthrough/openai/v1/batches`. Together: `/v1/v1/batches` upstream. OpenAI passthrough test will return once reconciled.

🐛 #166 — `/passthrough/anthropic` injects redundant `Authorization: Bearer` alongside x-api-key. NOT a credential leak (Bearer value is gateway's own provider secret, not caller's plaintext) but a wire-cleanliness gap vs docs §4.10 implication of x-api-key + anthropic-version only for Anthropic. Test pins the leak concern explicitly; the strict "Authorization absent" assertion is the verification once #166 is resolved.

Why this matters

Three real-world failure modes that prior coverage couldn't catch:

  • Auth-shape regression — a regression that forwarded `Authorization: Bearer` to Anthropic upstream as the only auth header would 401 in production.
  • Caller's bearer leak — the test verifies that even when Authorization IS present (per bug: /passthrough/anthropic injects redundant Authorization: Bearer <provider-secret> alongside x-api-key #166's wire-cleanliness gap), it does NOT contain the caller's plaintext bearer.
  • Body normalization regression — passthrough's whole point is "we don't normalize". A regression that JSON-round-tripped or schema-validated bodies on the way through would silently drop unknown fields, breaking batches/files use cases that depend on free-form bodies.

Source-blind discipline

Every assertion derives from external contracts:

No internal Rust paths or struct field names referenced.

Independent audit

Per CLAUDE.md §8, an independent audit agent reviewed the initial commit (4ffd636). Resolution log:

FindingSeverityResolution
Caller-bearer-leak assertion missing despite being a stated motivationMEDIUMTightened: upstream Authorization (if present) must not contain caller's plaintext bearer (7a2ed99)
`anthropic-version` pinned only as "non-empty string" — would pass on malformed values like `"v1"`MEDIUMPinned exact value `"2023-06-01"` per Anthropic docs (7a2ed99)
HTTP method matrix coverage gap (route is `ANY`; only POST tested)MEDIUMAcknowledged scope deferral
Provider matrix coverage gap (only anthropic; gemini/deepseek not tested)MEDIUMAcknowledged scope deferral; OpenAI held back as #164
Body verbatim assertion is string-equalityMEDIUMDeliberate strict contract per docs §4.10 — no change
L1-L3 informationalLOWNot addressed

The audit's recommended strict assertion (`Authorization === undefined`) revealed a separate gateway-side issue (#166): NOT a caller-bearer leak, but a redundant Bearer injection alongside x-api-key. The shipped assertion pins the leak concern explicitly while #166 tracks the wire-cleanliness gap.

Test plan

  • `npm test` (full e2e suite) — 33/33 passing locally (was 32)
  • No mock-data-only paths: case exercises the real `aisix` binary, real etcd config propagation, real fetch reverse-call against `/passthrough/anthropic/...`
  • CI green

Refs #151.

…forward (#151)
Third endpoint covered from #151's C7 row. Per gateway docs §4.10,
/passthrough/{provider}/*rest is the lowest-overhead escape hatch
for provider endpoints aisix doesn't natively wrap (OpenAI batches /
files / fine-tuning, Anthropic message-batches, etc.). Prior to
this file the gateway had **zero** e2e coverage on /passthrough —
every customer using batches/files had no regression protection on
the wire.
Case shipped:
Anthropic passthrough: caller POSTs
/passthrough/anthropic/v1/messages/batches with arbitrary body.
Gateway must:
- Strip /passthrough/anthropic prefix and forward the rest
(`/v1/messages/batches`) verbatim to upstream
- Inject Anthropic's auth shape: `x-api-key` + `anthropic-version`
headers (NOT `Authorization: Bearer` — a regression that
forwarded Bearer to Anthropic upstream would 401 in production
but pass against a permissive mock; the explicit auth header
pin is the only line of defence)
- Forward request body byte-for-byte (the entire point of
passthrough is "we don't normalize the body")
- Round-trip the upstream's response shape to the caller
Case held back: OpenAI passthrough surfaced a documented contract
conflict between docs/api-admin.md §4.3 (api_base example
`https://api.openai.com/v1`) and docs/api-proxy.md §4.10 (passthrough
example `/passthrough/openai/v1/batches`) — together they produce
double-/v1 upstream URL. Filed as #164. Same hold-back pattern as
#153 / #154 / #159 / #162.
References:
- Gateway's own /passthrough contract: `docs/api-proxy.md` §4.10
- Anthropic auth headers spec
<https://docs.anthropic.com/en/api/getting-started>
Refs #151
CopilotAI review requested due to automatic review settings May 9, 2026 13:58
@coderabbitai

coderabbitaiBot commented May 9, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@moonming has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 9 minutes and 11 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: ef642733-c724-4207-9d3a-6db4fe2bd416

📥 Commits

Reviewing files that changed from the base of the PR and between b691bf7 and 7a2ed99.

📒 Files selected for processing (1)
  • tests/e2e/src/cases/passthrough-e2e.test.ts

Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

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

CopilotAI 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.

Pull request overview

Adds end-to-end coverage for the gateway’s /passthrough/{provider}/*rest escape hatch, specifically pinning Anthropic passthrough behavior to prevent auth-shape and wire-format regressions.

Changes:

  • Introduces a new e2e test covering POST /passthrough/anthropic/v1/messages/batches.
  • Asserts passthrough path stripping (upstream sees /v1/messages/batches) and verbatim request-body forwarding.
  • Asserts Anthropic-specific auth-shape switching (x-api-key + non-empty anthropic-version) for upstream requests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

expect(typeof testCalls[0]?.headers["anthropic-version"]).toBe("string");
expect(
(testCalls[0]?.headers["anthropic-version"] as string).length,
).toBeGreaterThan(0);
Audit (per CLAUDE.md §8) found two MEDIUM:
MEDIUM M1 (caller-bearer-leak assertion missing despite being one
of the PR's stated motivations):
Initial PR's assertions verified `x-api-key: sk-ant-mock` correct
injection but did NOT pin that the caller's plaintext bearer was
absent on the upstream side — a regression that injected
x-api-key correctly AND forwarded the caller's Bearer would
silently pass.
Audit recommended `expect(authorization).toBeUndefined()`. That
assertion FAILED (gateway today sends `Authorization: Bearer
sk-ant-mock` redundantly alongside x-api-key) — surfacing a
separate issue: NOT a caller-bearer leak (the value is the
gateway's own provider secret, not the caller's plaintext), but
a wire-cleanliness gap vs docs §4.10 which implies x-api-key +
anthropic-version only for Anthropic. Filed separately as #166.
Settled assertion: pin the LEAK concern explicitly — if the
upstream Authorization is present, its value must NOT contain
the caller's plaintext bearer. Catches the regression mode the
PR description claimed to defend against.
MEDIUM M2 (anthropic-version pinned only as "non-empty string"):
Pinned exact value `"2023-06-01"` per Anthropic's documented
current API version
(<https://docs.anthropic.com/en/api/getting-started>). A
regression that injected a malformed-but-non-empty value (e.g.
"v1", "latest", "unspecified") would 400 against real Anthropic
but pass against the permissive mock without this exact pin.
MEDIUM M3-M4 (HTTP method matrix, provider matrix gaps):
Acknowledged scope deferrals; not addressed in this PR. Worth
filing as separate follow-up.
LOW L1-L3: not addressed.
All 33 e2e tests still pass locally.
@moonming
moonming merged commit 971d119 into mainMay 9, 2026
10 of 11 checks passed
@moonming
moonming deleted the test/e2e-c7-passthrough branch May 9, 2026 14:15
moonming added a commit that referenced this pull request May 9, 2026
Closes#157 (test-infra concern, not a product bug).
Issue: the e2e suite's `guardrail-keyword-e2e.test.ts` has flaked
three times on CI in the past 24 hours (#157 first occurrence,
plus reruns required on PR #165 and #167). Failure mode is the
same: `waitConfigPropagation: condition not met within 5s`.
Root cause: vitest is configured with `maxForks: 4` (per
`vitest.config.ts`), so up to 4 test files run in parallel, each
spawning its own `aisix` binary against a SHARED etcd. Each
binary opens watches and writes resources via the admin API
concurrently with the others. Under that load, etcd watch
dispatch latency for the LAST resource in a multi-resource
batch (e.g. a Guardrail rule following Model + ApiKey +
ProviderKey writes) can exceed the 5s budget.
The 5s budget was sized when the suite had ~9 files. The suite
is now 20+ files (#161, #163, #165, #167 added embeddings,
responses, passthrough, rerank, images). The growth in
parallelism load wasn't matched by a budget bump.
Fix: raise the deadline to 10s. This:
- Eliminates the recurring rerun churn on feature PRs
- Preserves the "fail loudly on a genuinely stuck snapshot"
property — 10s is still a generous floor; a real bug where
propagation hangs indefinitely would still fail clearly
- Doesn't change the happy-path latency at all (the helper
polls every 50ms and returns as soon as the condition is
met — bumping the deadline only affects the sad path)
This is a test-infra-only change; no product behavior is affected.
The ≤500ms spec target for in-process propagation is unchanged;
this is purely the CI test harness's wait budget for slow runners
under concurrent load.
If 10s proves insufficient as the suite grows further, the next
escalation would be to reduce `maxForks` from 4 to 2 (slower wall
time, less etcd pressure) — tracked in #157 as a fallback.
moonming added a commit that referenced this pull request May 9, 2026
… (#169)
* test(harness): raise waitConfigPropagation budget 5s → 10s (#157)
Closes#157 (test-infra concern, not a product bug).
Issue: the e2e suite's `guardrail-keyword-e2e.test.ts` has flaked
three times on CI in the past 24 hours (#157 first occurrence,
plus reruns required on PR #165 and #167). Failure mode is the
same: `waitConfigPropagation: condition not met within 5s`.
Root cause: vitest is configured with `maxForks: 4` (per
`vitest.config.ts`), so up to 4 test files run in parallel, each
spawning its own `aisix` binary against a SHARED etcd. Each
binary opens watches and writes resources via the admin API
concurrently with the others. Under that load, etcd watch
dispatch latency for the LAST resource in a multi-resource
batch (e.g. a Guardrail rule following Model + ApiKey +
ProviderKey writes) can exceed the 5s budget.
The 5s budget was sized when the suite had ~9 files. The suite
is now 20+ files (#161, #163, #165, #167 added embeddings,
responses, passthrough, rerank, images). The growth in
parallelism load wasn't matched by a budget bump.
Fix: raise the deadline to 10s. This:
- Eliminates the recurring rerun churn on feature PRs
- Preserves the "fail loudly on a genuinely stuck snapshot"
property — 10s is still a generous floor; a real bug where
propagation hangs indefinitely would still fail clearly
- Doesn't change the happy-path latency at all (the helper
polls every 50ms and returns as soon as the condition is
met — bumping the deadline only affects the sad path)
This is a test-infra-only change; no product behavior is affected.
The ≤500ms spec target for in-process propagation is unchanged;
this is purely the CI test harness's wait budget for slow runners
under concurrent load.
If 10s proves insufficient as the suite grows further, the next
escalation would be to reduce `maxForks` from 4 to 2 (slower wall
time, less etcd pressure) — tracked in #157 as a fallback.
* test(harness): also reduce maxForks 4→2 (#157 fallback escalation)
After the timeout bump landed, #169's own CI run still flaked with
the same `condition not met within 10s` error — meaning etcd watch
dispatch latency genuinely exceeds 10s under maxForks=4 with the
current suite size (20+ files). The audit on #169 had flagged this
as the "product-side hypothesis still open" — confirmed.
Apply the documented fallback from #157: cut maxForks from 4 to 2.
This halves the concurrent-watcher count against the shared etcd
and brings dispatch latency back inside the budget.
Trade-off: wall time grows ~1.5-2× per CI run (locally measured
~14s @ maxForks=4 vs ~30s @ maxForks=2, so faster than expected
because the extra parallelism wasn't fully utilized anyway —
contention dominated). Net for CI is "predictable green" vs
"fast but constantly-rerunning".
The 10s `waitConfigPropagation` budget from the prior commit stays
in place as belt-and-suspenders — even with maxForks=2 the original
5s would still be tight on slow runners.
Combined this PR now does:
1. waitConfigPropagation deadline 5s → 10s (`harness/admin.ts`)
2. maxForks 4 → 2 (`vitest.config.ts`)
The product-side hypothesis ("etcd watch dispatch degrades
non-linearly with concurrent watchers") is now strongly supported
and worth a separate product-side investigation tracked in #157
follow-up.
moonming added a commit that referenced this pull request May 10, 2026
#166) (#209)
## Problem
Per `docs/api-proxy.md` §4.10, Anthropic passthrough was supposed to
inject the documented Anthropic auth shape — `x-api-key` +
`anthropic-version` — and only that. In practice the gateway was
ALSO injecting `Authorization: Bearer <provider-secret>` alongside:
```
x-api-key: sk-ant-mock ← correct per Anthropic spec
anthropic-version: 2023-06-01 ← correct per Anthropic spec
Authorization: Bearer sk-ant-mock ← redundant, not in Anthropic's auth contract
```
Real Anthropic ignores the redundant Bearer today, but:
- Future stricter Anthropic gateways or customer-side middleware
could reject the request based on having unexpected auth headers.
- Operators inspecting upstream traffic captures generate
"is this a leak?" tickets every time the redundant header
surfaces — even though the value is the gateway's own provider
secret, not the caller's plaintext bearer.
Not a credential leak (verified by #165's existing assertion that
the caller's plaintext bearer never reaches the upstream), but a
docs-vs-behavior gap that operators shouldn't have to investigate.
## Fix
In `crates/aisix-proxy/src/passthrough.rs::dispatch`, restructure the
auth-injection branch so each provider gets EXACTLY ONE auth shape:
- openai / gemini / deepseek / unknown providers → `Authorization: Bearer …`
- anthropic → `x-api-key` + `anthropic-version` ONLY
- empty `api_key` → no auth headers at all (was previously: still
injected an empty `x-api-key` for anthropic — also cleaned up)
```rust
if api_key.is_empty() {
// nothing to inject
} else if provider_lower == "anthropic" {
builder = builder.header("x-api-key", &api_key);
builder = builder.header("anthropic-version", "2023-06-01");
} else {
builder = builder.header(header::AUTHORIZATION, format!("Bearer {api_key}"));
}
```
## Tests
**Rust unit (`crates/aisix-proxy/src/passthrough.rs`)** — new
`anthropic_passthrough_does_not_inject_bearer_auth` test:
- Configures Anthropic passthrough with secret `sk-ant-test`
- Posts to `/passthrough/anthropic/v1/messages/batches`
- wiremock subset-match on `x-api-key` + `anthropic-version`
- **Strict assertion**: drains `received_requests` and asserts
`!headers.contains_key("authorization")` — the redundant Bearer
must NOT be on the wire
- Sanity: positive value-pin on `x-api-key: sk-ant-test` and
`anthropic-version: 2023-06-01`
**E2E (`tests/e2e/src/cases/passthrough-e2e.test.ts`)** — tightened
the existing Anthropic case from the lenient
`if (auth !== undefined) expect(auth).not.toContain(CALLER_PLAINTEXT)`
to the strict `expect(headers["authorization"]).toBeUndefined()`.
The lenient version caught caller-plaintext leaks (covered by #165);
the strict version catches the wire-cleanliness regression #166
fixes.
Helpers added: `anthropic_model()` and
`anthropic_provider_key_entry()` for the Anthropic-shaped fixtures
(parallel to the existing `openai_model` / `provider_key_entry`).
## Verification
- `cargo test -p aisix-proxy --lib`: **152/152 passing** (incl. new test)
- `cargo clippy -p aisix-proxy --lib --tests -- -D warnings`: clean
- `cargo fmt --check`: clean
- `pnpm tsc --noEmit` (e2e): clean
## References
- Issue: #166
- `docs/api-proxy.md` §4.10 (passthrough contract)
- Anthropic auth spec: <https://docs.anthropic.com/en/api/getting-started>
Sign up for freeto 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.

2 participants

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

test(e2e): C7 /passthrough/anthropic auth-shape switching + verbatim forward - #165

Merged
moonming merged 2 commits into
mainfrom
test/e2e-c7-passthrough
May 9, 2026
Merged

test(e2e): C7 /passthrough/anthropic auth-shape switching + verbatim forward#165
moonming merged 2 commits into
mainfrom
test/e2e-c7-passthrough

Conversation

@moonming

@moonmingmoonming commented May 9, 2026

Copy link
Copy Markdown
Member

Summary

Third endpoint covered from #151's C7 row. Per gateway docs §4.10, `/passthrough/{provider}/*rest` is the lowest-overhead escape hatch for provider endpoints aisix doesn't natively wrap (OpenAI batches / files / fine-tuning, Anthropic message-batches). Prior to this PR the gateway had zero e2e coverage on `/passthrough`.

One of two originally-planned cases ships here. The other surfaced a docs/behavior conflict and is held back as #164.

What's pinned

CaseUser journeyAsserts
Anthropic passthroughPOST `/passthrough/anthropic/v1/messages/batches` with arbitrary bodyCaller sees upstream's response back; upstream got POST at `/v1/messages/batches` (path verbatim after `/passthrough/anthropic` stripped); auth-shape switching: `x-api-key: sk-ant-mock` (NOT caller's bearer) + `anthropic-version: 2023-06-01` (Anthropic's documented current version); upstream Authorization (if present) does not contain caller's plaintext bearer (no credential leak); body forwarded byte-for-byte

Held back as separate issues

🐛 #164 — `/passthrough/openai/*` doubles `/v1` prefix when api_base ends with `/v1` (contradicts published docs). `docs/api-admin.md` §4.3 publishes `api_base: "https://api.openai.com/v1"\`; `docs/api-proxy.md` §4.10 publishes the call as `/passthrough/openai/v1/batches`. Together: `/v1/v1/batches` upstream. OpenAI passthrough test will return once reconciled.

🐛 #166 — `/passthrough/anthropic` injects redundant `Authorization: Bearer` alongside x-api-key. NOT a credential leak (Bearer value is gateway's own provider secret, not caller's plaintext) but a wire-cleanliness gap vs docs §4.10 implication of x-api-key + anthropic-version only for Anthropic. Test pins the leak concern explicitly; the strict "Authorization absent" assertion is the verification once #166 is resolved.

Why this matters

Three real-world failure modes that prior coverage couldn't catch:

  • Auth-shape regression — a regression that forwarded `Authorization: Bearer` to Anthropic upstream as the only auth header would 401 in production.
  • Caller's bearer leak — the test verifies that even when Authorization IS present (per bug: /passthrough/anthropic injects redundant Authorization: Bearer <provider-secret> alongside x-api-key #166's wire-cleanliness gap), it does NOT contain the caller's plaintext bearer.
  • Body normalization regression — passthrough's whole point is "we don't normalize". A regression that JSON-round-tripped or schema-validated bodies on the way through would silently drop unknown fields, breaking batches/files use cases that depend on free-form bodies.

Source-blind discipline

Every assertion derives from external contracts:

No internal Rust paths or struct field names referenced.

Independent audit

Per CLAUDE.md §8, an independent audit agent reviewed the initial commit (4ffd636). Resolution log:

FindingSeverityResolution
Caller-bearer-leak assertion missing despite being a stated motivationMEDIUMTightened: upstream Authorization (if present) must not contain caller's plaintext bearer (7a2ed99)
`anthropic-version` pinned only as "non-empty string" — would pass on malformed values like `"v1"`MEDIUMPinned exact value `"2023-06-01"` per Anthropic docs (7a2ed99)
HTTP method matrix coverage gap (route is `ANY`; only POST tested)MEDIUMAcknowledged scope deferral
Provider matrix coverage gap (only anthropic; gemini/deepseek not tested)MEDIUMAcknowledged scope deferral; OpenAI held back as #164
Body verbatim assertion is string-equalityMEDIUMDeliberate strict contract per docs §4.10 — no change
L1-L3 informationalLOWNot addressed

The audit's recommended strict assertion (`Authorization === undefined`) revealed a separate gateway-side issue (#166): NOT a caller-bearer leak, but a redundant Bearer injection alongside x-api-key. The shipped assertion pins the leak concern explicitly while #166 tracks the wire-cleanliness gap.

Test plan

  • `npm test` (full e2e suite) — 33/33 passing locally (was 32)
  • No mock-data-only paths: case exercises the real `aisix` binary, real etcd config propagation, real fetch reverse-call against `/passthrough/anthropic/...`
  • CI green

Refs #151.

…forward (#151)
Third endpoint covered from #151's C7 row. Per gateway docs §4.10,
/passthrough/{provider}/*rest is the lowest-overhead escape hatch
for provider endpoints aisix doesn't natively wrap (OpenAI batches /
files / fine-tuning, Anthropic message-batches, etc.). Prior to
this file the gateway had **zero** e2e coverage on /passthrough —
every customer using batches/files had no regression protection on
the wire.
Case shipped:
Anthropic passthrough: caller POSTs
/passthrough/anthropic/v1/messages/batches with arbitrary body.
Gateway must:
- Strip /passthrough/anthropic prefix and forward the rest
(`/v1/messages/batches`) verbatim to upstream
- Inject Anthropic's auth shape: `x-api-key` + `anthropic-version`
headers (NOT `Authorization: Bearer` — a regression that
forwarded Bearer to Anthropic upstream would 401 in production
but pass against a permissive mock; the explicit auth header
pin is the only line of defence)
- Forward request body byte-for-byte (the entire point of
passthrough is "we don't normalize the body")
- Round-trip the upstream's response shape to the caller
Case held back: OpenAI passthrough surfaced a documented contract
conflict between docs/api-admin.md §4.3 (api_base example
`https://api.openai.com/v1`) and docs/api-proxy.md §4.10 (passthrough
example `/passthrough/openai/v1/batches`) — together they produce
double-/v1 upstream URL. Filed as #164. Same hold-back pattern as
#153 / #154 / #159 / #162.
References:
- Gateway's own /passthrough contract: `docs/api-proxy.md` §4.10
- Anthropic auth headers spec
<https://docs.anthropic.com/en/api/getting-started>
Refs #151
CopilotAI review requested due to automatic review settings May 9, 2026 13:58
@coderabbitai

coderabbitaiBot commented May 9, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@moonming has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 9 minutes and 11 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: ef642733-c724-4207-9d3a-6db4fe2bd416

📥 Commits

Reviewing files that changed from the base of the PR and between b691bf7 and 7a2ed99.

📒 Files selected for processing (1)
  • tests/e2e/src/cases/passthrough-e2e.test.ts

Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

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

CopilotAI 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.

Pull request overview

Adds end-to-end coverage for the gateway’s /passthrough/{provider}/*rest escape hatch, specifically pinning Anthropic passthrough behavior to prevent auth-shape and wire-format regressions.

Changes:

  • Introduces a new e2e test covering POST /passthrough/anthropic/v1/messages/batches.
  • Asserts passthrough path stripping (upstream sees /v1/messages/batches) and verbatim request-body forwarding.
  • Asserts Anthropic-specific auth-shape switching (x-api-key + non-empty anthropic-version) for upstream requests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

expect(typeof testCalls[0]?.headers["anthropic-version"]).toBe("string");
expect(
(testCalls[0]?.headers["anthropic-version"] as string).length,
).toBeGreaterThan(0);
Audit (per CLAUDE.md §8) found two MEDIUM:
MEDIUM M1 (caller-bearer-leak assertion missing despite being one
of the PR's stated motivations):
Initial PR's assertions verified `x-api-key: sk-ant-mock` correct
injection but did NOT pin that the caller's plaintext bearer was
absent on the upstream side — a regression that injected
x-api-key correctly AND forwarded the caller's Bearer would
silently pass.
Audit recommended `expect(authorization).toBeUndefined()`. That
assertion FAILED (gateway today sends `Authorization: Bearer
sk-ant-mock` redundantly alongside x-api-key) — surfacing a
separate issue: NOT a caller-bearer leak (the value is the
gateway's own provider secret, not the caller's plaintext), but
a wire-cleanliness gap vs docs §4.10 which implies x-api-key +
anthropic-version only for Anthropic. Filed separately as #166.
Settled assertion: pin the LEAK concern explicitly — if the
upstream Authorization is present, its value must NOT contain
the caller's plaintext bearer. Catches the regression mode the
PR description claimed to defend against.
MEDIUM M2 (anthropic-version pinned only as "non-empty string"):
Pinned exact value `"2023-06-01"` per Anthropic's documented
current API version
(<https://docs.anthropic.com/en/api/getting-started>). A
regression that injected a malformed-but-non-empty value (e.g.
"v1", "latest", "unspecified") would 400 against real Anthropic
but pass against the permissive mock without this exact pin.
MEDIUM M3-M4 (HTTP method matrix, provider matrix gaps):
Acknowledged scope deferrals; not addressed in this PR. Worth
filing as separate follow-up.
LOW L1-L3: not addressed.
All 33 e2e tests still pass locally.
@moonming
moonming merged commit 971d119 into mainMay 9, 2026
10 of 11 checks passed
@moonming
moonming deleted the test/e2e-c7-passthrough branch May 9, 2026 14:15
moonming added a commit that referenced this pull request May 9, 2026
Closes#157 (test-infra concern, not a product bug).
Issue: the e2e suite's `guardrail-keyword-e2e.test.ts` has flaked
three times on CI in the past 24 hours (#157 first occurrence,
plus reruns required on PR #165 and #167). Failure mode is the
same: `waitConfigPropagation: condition not met within 5s`.
Root cause: vitest is configured with `maxForks: 4` (per
`vitest.config.ts`), so up to 4 test files run in parallel, each
spawning its own `aisix` binary against a SHARED etcd. Each
binary opens watches and writes resources via the admin API
concurrently with the others. Under that load, etcd watch
dispatch latency for the LAST resource in a multi-resource
batch (e.g. a Guardrail rule following Model + ApiKey +
ProviderKey writes) can exceed the 5s budget.
The 5s budget was sized when the suite had ~9 files. The suite
is now 20+ files (#161, #163, #165, #167 added embeddings,
responses, passthrough, rerank, images). The growth in
parallelism load wasn't matched by a budget bump.
Fix: raise the deadline to 10s. This:
- Eliminates the recurring rerun churn on feature PRs
- Preserves the "fail loudly on a genuinely stuck snapshot"
property — 10s is still a generous floor; a real bug where
propagation hangs indefinitely would still fail clearly
- Doesn't change the happy-path latency at all (the helper
polls every 50ms and returns as soon as the condition is
met — bumping the deadline only affects the sad path)
This is a test-infra-only change; no product behavior is affected.
The ≤500ms spec target for in-process propagation is unchanged;
this is purely the CI test harness's wait budget for slow runners
under concurrent load.
If 10s proves insufficient as the suite grows further, the next
escalation would be to reduce `maxForks` from 4 to 2 (slower wall
time, less etcd pressure) — tracked in #157 as a fallback.
moonming added a commit that referenced this pull request May 9, 2026
… (#169)
* test(harness): raise waitConfigPropagation budget 5s → 10s (#157)
Closes#157 (test-infra concern, not a product bug).
Issue: the e2e suite's `guardrail-keyword-e2e.test.ts` has flaked
three times on CI in the past 24 hours (#157 first occurrence,
plus reruns required on PR #165 and #167). Failure mode is the
same: `waitConfigPropagation: condition not met within 5s`.
Root cause: vitest is configured with `maxForks: 4` (per
`vitest.config.ts`), so up to 4 test files run in parallel, each
spawning its own `aisix` binary against a SHARED etcd. Each
binary opens watches and writes resources via the admin API
concurrently with the others. Under that load, etcd watch
dispatch latency for the LAST resource in a multi-resource
batch (e.g. a Guardrail rule following Model + ApiKey +
ProviderKey writes) can exceed the 5s budget.
The 5s budget was sized when the suite had ~9 files. The suite
is now 20+ files (#161, #163, #165, #167 added embeddings,
responses, passthrough, rerank, images). The growth in
parallelism load wasn't matched by a budget bump.
Fix: raise the deadline to 10s. This:
- Eliminates the recurring rerun churn on feature PRs
- Preserves the "fail loudly on a genuinely stuck snapshot"
property — 10s is still a generous floor; a real bug where
propagation hangs indefinitely would still fail clearly
- Doesn't change the happy-path latency at all (the helper
polls every 50ms and returns as soon as the condition is
met — bumping the deadline only affects the sad path)
This is a test-infra-only change; no product behavior is affected.
The ≤500ms spec target for in-process propagation is unchanged;
this is purely the CI test harness's wait budget for slow runners
under concurrent load.
If 10s proves insufficient as the suite grows further, the next
escalation would be to reduce `maxForks` from 4 to 2 (slower wall
time, less etcd pressure) — tracked in #157 as a fallback.
* test(harness): also reduce maxForks 4→2 (#157 fallback escalation)
After the timeout bump landed, #169's own CI run still flaked with
the same `condition not met within 10s` error — meaning etcd watch
dispatch latency genuinely exceeds 10s under maxForks=4 with the
current suite size (20+ files). The audit on #169 had flagged this
as the "product-side hypothesis still open" — confirmed.
Apply the documented fallback from #157: cut maxForks from 4 to 2.
This halves the concurrent-watcher count against the shared etcd
and brings dispatch latency back inside the budget.
Trade-off: wall time grows ~1.5-2× per CI run (locally measured
~14s @ maxForks=4 vs ~30s @ maxForks=2, so faster than expected
because the extra parallelism wasn't fully utilized anyway —
contention dominated). Net for CI is "predictable green" vs
"fast but constantly-rerunning".
The 10s `waitConfigPropagation` budget from the prior commit stays
in place as belt-and-suspenders — even with maxForks=2 the original
5s would still be tight on slow runners.
Combined this PR now does:
1. waitConfigPropagation deadline 5s → 10s (`harness/admin.ts`)
2. maxForks 4 → 2 (`vitest.config.ts`)
The product-side hypothesis ("etcd watch dispatch degrades
non-linearly with concurrent watchers") is now strongly supported
and worth a separate product-side investigation tracked in #157
follow-up.
moonming added a commit that referenced this pull request May 10, 2026
#166) (#209)
## Problem
Per `docs/api-proxy.md` §4.10, Anthropic passthrough was supposed to
inject the documented Anthropic auth shape — `x-api-key` +
`anthropic-version` — and only that. In practice the gateway was
ALSO injecting `Authorization: Bearer <provider-secret>` alongside:
```
x-api-key: sk-ant-mock ← correct per Anthropic spec
anthropic-version: 2023-06-01 ← correct per Anthropic spec
Authorization: Bearer sk-ant-mock ← redundant, not in Anthropic's auth contract
```
Real Anthropic ignores the redundant Bearer today, but:
- Future stricter Anthropic gateways or customer-side middleware
could reject the request based on having unexpected auth headers.
- Operators inspecting upstream traffic captures generate
"is this a leak?" tickets every time the redundant header
surfaces — even though the value is the gateway's own provider
secret, not the caller's plaintext bearer.
Not a credential leak (verified by #165's existing assertion that
the caller's plaintext bearer never reaches the upstream), but a
docs-vs-behavior gap that operators shouldn't have to investigate.
## Fix
In `crates/aisix-proxy/src/passthrough.rs::dispatch`, restructure the
auth-injection branch so each provider gets EXACTLY ONE auth shape:
- openai / gemini / deepseek / unknown providers → `Authorization: Bearer …`
- anthropic → `x-api-key` + `anthropic-version` ONLY
- empty `api_key` → no auth headers at all (was previously: still
injected an empty `x-api-key` for anthropic — also cleaned up)
```rust
if api_key.is_empty() {
// nothing to inject
} else if provider_lower == "anthropic" {
builder = builder.header("x-api-key", &api_key);
builder = builder.header("anthropic-version", "2023-06-01");
} else {
builder = builder.header(header::AUTHORIZATION, format!("Bearer {api_key}"));
}
```
## Tests
**Rust unit (`crates/aisix-proxy/src/passthrough.rs`)** — new
`anthropic_passthrough_does_not_inject_bearer_auth` test:
- Configures Anthropic passthrough with secret `sk-ant-test`
- Posts to `/passthrough/anthropic/v1/messages/batches`
- wiremock subset-match on `x-api-key` + `anthropic-version`
- **Strict assertion**: drains `received_requests` and asserts
`!headers.contains_key("authorization")` — the redundant Bearer
must NOT be on the wire
- Sanity: positive value-pin on `x-api-key: sk-ant-test` and
`anthropic-version: 2023-06-01`
**E2E (`tests/e2e/src/cases/passthrough-e2e.test.ts`)** — tightened
the existing Anthropic case from the lenient
`if (auth !== undefined) expect(auth).not.toContain(CALLER_PLAINTEXT)`
to the strict `expect(headers["authorization"]).toBeUndefined()`.
The lenient version caught caller-plaintext leaks (covered by #165);
the strict version catches the wire-cleanliness regression #166
fixes.
Helpers added: `anthropic_model()` and
`anthropic_provider_key_entry()` for the Anthropic-shaped fixtures
(parallel to the existing `openai_model` / `provider_key_entry`).
## Verification
- `cargo test -p aisix-proxy --lib`: **152/152 passing** (incl. new test)
- `cargo clippy -p aisix-proxy --lib --tests -- -D warnings`: clean
- `cargo fmt --check`: clean
- `pnpm tsc --noEmit` (e2e): clean
## References
- Issue: #166
- `docs/api-proxy.md` §4.10 (passthrough contract)
- Anthropic auth spec: <https://docs.anthropic.com/en/api/getting-started>
Sign up for freeto 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.

2 participants

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

test(e2e): C7 /passthrough/anthropic auth-shape switching + verbatim forward - #165

Merged
moonming merged 2 commits into
mainfrom
test/e2e-c7-passthrough
May 9, 2026
Merged

test(e2e): C7 /passthrough/anthropic auth-shape switching + verbatim forward#165
moonming merged 2 commits into
mainfrom
test/e2e-c7-passthrough

Conversation

@moonming

@moonmingmoonming commented May 9, 2026

Copy link
Copy Markdown
Member

Summary

Third endpoint covered from #151's C7 row. Per gateway docs §4.10, `/passthrough/{provider}/*rest` is the lowest-overhead escape hatch for provider endpoints aisix doesn't natively wrap (OpenAI batches / files / fine-tuning, Anthropic message-batches). Prior to this PR the gateway had zero e2e coverage on `/passthrough`.

One of two originally-planned cases ships here. The other surfaced a docs/behavior conflict and is held back as #164.

What's pinned

CaseUser journeyAsserts
Anthropic passthroughPOST `/passthrough/anthropic/v1/messages/batches` with arbitrary bodyCaller sees upstream's response back; upstream got POST at `/v1/messages/batches` (path verbatim after `/passthrough/anthropic` stripped); auth-shape switching: `x-api-key: sk-ant-mock` (NOT caller's bearer) + `anthropic-version: 2023-06-01` (Anthropic's documented current version); upstream Authorization (if present) does not contain caller's plaintext bearer (no credential leak); body forwarded byte-for-byte

Held back as separate issues

🐛 #164 — `/passthrough/openai/*` doubles `/v1` prefix when api_base ends with `/v1` (contradicts published docs). `docs/api-admin.md` §4.3 publishes `api_base: "https://api.openai.com/v1"\`; `docs/api-proxy.md` §4.10 publishes the call as `/passthrough/openai/v1/batches`. Together: `/v1/v1/batches` upstream. OpenAI passthrough test will return once reconciled.

🐛 #166 — `/passthrough/anthropic` injects redundant `Authorization: Bearer` alongside x-api-key. NOT a credential leak (Bearer value is gateway's own provider secret, not caller's plaintext) but a wire-cleanliness gap vs docs §4.10 implication of x-api-key + anthropic-version only for Anthropic. Test pins the leak concern explicitly; the strict "Authorization absent" assertion is the verification once #166 is resolved.

Why this matters

Three real-world failure modes that prior coverage couldn't catch:

  • Auth-shape regression — a regression that forwarded `Authorization: Bearer` to Anthropic upstream as the only auth header would 401 in production.
  • Caller's bearer leak — the test verifies that even when Authorization IS present (per bug: /passthrough/anthropic injects redundant Authorization: Bearer <provider-secret> alongside x-api-key #166's wire-cleanliness gap), it does NOT contain the caller's plaintext bearer.
  • Body normalization regression — passthrough's whole point is "we don't normalize". A regression that JSON-round-tripped or schema-validated bodies on the way through would silently drop unknown fields, breaking batches/files use cases that depend on free-form bodies.

Source-blind discipline

Every assertion derives from external contracts:

No internal Rust paths or struct field names referenced.

Independent audit

Per CLAUDE.md §8, an independent audit agent reviewed the initial commit (4ffd636). Resolution log:

FindingSeverityResolution
Caller-bearer-leak assertion missing despite being a stated motivationMEDIUMTightened: upstream Authorization (if present) must not contain caller's plaintext bearer (7a2ed99)
`anthropic-version` pinned only as "non-empty string" — would pass on malformed values like `"v1"`MEDIUMPinned exact value `"2023-06-01"` per Anthropic docs (7a2ed99)
HTTP method matrix coverage gap (route is `ANY`; only POST tested)MEDIUMAcknowledged scope deferral
Provider matrix coverage gap (only anthropic; gemini/deepseek not tested)MEDIUMAcknowledged scope deferral; OpenAI held back as #164
Body verbatim assertion is string-equalityMEDIUMDeliberate strict contract per docs §4.10 — no change
L1-L3 informationalLOWNot addressed

The audit's recommended strict assertion (`Authorization === undefined`) revealed a separate gateway-side issue (#166): NOT a caller-bearer leak, but a redundant Bearer injection alongside x-api-key. The shipped assertion pins the leak concern explicitly while #166 tracks the wire-cleanliness gap.

Test plan

  • `npm test` (full e2e suite) — 33/33 passing locally (was 32)
  • No mock-data-only paths: case exercises the real `aisix` binary, real etcd config propagation, real fetch reverse-call against `/passthrough/anthropic/...`
  • CI green

Refs #151.

…forward (#151)
Third endpoint covered from #151's C7 row. Per gateway docs §4.10,
/passthrough/{provider}/*rest is the lowest-overhead escape hatch
for provider endpoints aisix doesn't natively wrap (OpenAI batches /
files / fine-tuning, Anthropic message-batches, etc.). Prior to
this file the gateway had **zero** e2e coverage on /passthrough —
every customer using batches/files had no regression protection on
the wire.
Case shipped:
Anthropic passthrough: caller POSTs
/passthrough/anthropic/v1/messages/batches with arbitrary body.
Gateway must:
- Strip /passthrough/anthropic prefix and forward the rest
(`/v1/messages/batches`) verbatim to upstream
- Inject Anthropic's auth shape: `x-api-key` + `anthropic-version`
headers (NOT `Authorization: Bearer` — a regression that
forwarded Bearer to Anthropic upstream would 401 in production
but pass against a permissive mock; the explicit auth header
pin is the only line of defence)
- Forward request body byte-for-byte (the entire point of
passthrough is "we don't normalize the body")
- Round-trip the upstream's response shape to the caller
Case held back: OpenAI passthrough surfaced a documented contract
conflict between docs/api-admin.md §4.3 (api_base example
`https://api.openai.com/v1`) and docs/api-proxy.md §4.10 (passthrough
example `/passthrough/openai/v1/batches`) — together they produce
double-/v1 upstream URL. Filed as #164. Same hold-back pattern as
#153 / #154 / #159 / #162.
References:
- Gateway's own /passthrough contract: `docs/api-proxy.md` §4.10
- Anthropic auth headers spec
<https://docs.anthropic.com/en/api/getting-started>
Refs #151
CopilotAI review requested due to automatic review settings May 9, 2026 13:58
@coderabbitai

coderabbitaiBot commented May 9, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@moonming has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 9 minutes and 11 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: ef642733-c724-4207-9d3a-6db4fe2bd416

📥 Commits

Reviewing files that changed from the base of the PR and between b691bf7 and 7a2ed99.

📒 Files selected for processing (1)
  • tests/e2e/src/cases/passthrough-e2e.test.ts

Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

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

CopilotAI 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.

Pull request overview

Adds end-to-end coverage for the gateway’s /passthrough/{provider}/*rest escape hatch, specifically pinning Anthropic passthrough behavior to prevent auth-shape and wire-format regressions.

Changes:

  • Introduces a new e2e test covering POST /passthrough/anthropic/v1/messages/batches.
  • Asserts passthrough path stripping (upstream sees /v1/messages/batches) and verbatim request-body forwarding.
  • Asserts Anthropic-specific auth-shape switching (x-api-key + non-empty anthropic-version) for upstream requests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

expect(typeof testCalls[0]?.headers["anthropic-version"]).toBe("string");
expect(
(testCalls[0]?.headers["anthropic-version"] as string).length,
).toBeGreaterThan(0);
Audit (per CLAUDE.md §8) found two MEDIUM:
MEDIUM M1 (caller-bearer-leak assertion missing despite being one
of the PR's stated motivations):
Initial PR's assertions verified `x-api-key: sk-ant-mock` correct
injection but did NOT pin that the caller's plaintext bearer was
absent on the upstream side — a regression that injected
x-api-key correctly AND forwarded the caller's Bearer would
silently pass.
Audit recommended `expect(authorization).toBeUndefined()`. That
assertion FAILED (gateway today sends `Authorization: Bearer
sk-ant-mock` redundantly alongside x-api-key) — surfacing a
separate issue: NOT a caller-bearer leak (the value is the
gateway's own provider secret, not the caller's plaintext), but
a wire-cleanliness gap vs docs §4.10 which implies x-api-key +
anthropic-version only for Anthropic. Filed separately as #166.
Settled assertion: pin the LEAK concern explicitly — if the
upstream Authorization is present, its value must NOT contain
the caller's plaintext bearer. Catches the regression mode the
PR description claimed to defend against.
MEDIUM M2 (anthropic-version pinned only as "non-empty string"):
Pinned exact value `"2023-06-01"` per Anthropic's documented
current API version
(<https://docs.anthropic.com/en/api/getting-started>). A
regression that injected a malformed-but-non-empty value (e.g.
"v1", "latest", "unspecified") would 400 against real Anthropic
but pass against the permissive mock without this exact pin.
MEDIUM M3-M4 (HTTP method matrix, provider matrix gaps):
Acknowledged scope deferrals; not addressed in this PR. Worth
filing as separate follow-up.
LOW L1-L3: not addressed.
All 33 e2e tests still pass locally.
@moonming
moonming merged commit 971d119 into mainMay 9, 2026
10 of 11 checks passed
@moonming
moonming deleted the test/e2e-c7-passthrough branch May 9, 2026 14:15
moonming added a commit that referenced this pull request May 9, 2026
Closes#157 (test-infra concern, not a product bug).
Issue: the e2e suite's `guardrail-keyword-e2e.test.ts` has flaked
three times on CI in the past 24 hours (#157 first occurrence,
plus reruns required on PR #165 and #167). Failure mode is the
same: `waitConfigPropagation: condition not met within 5s`.
Root cause: vitest is configured with `maxForks: 4` (per
`vitest.config.ts`), so up to 4 test files run in parallel, each
spawning its own `aisix` binary against a SHARED etcd. Each
binary opens watches and writes resources via the admin API
concurrently with the others. Under that load, etcd watch
dispatch latency for the LAST resource in a multi-resource
batch (e.g. a Guardrail rule following Model + ApiKey +
ProviderKey writes) can exceed the 5s budget.
The 5s budget was sized when the suite had ~9 files. The suite
is now 20+ files (#161, #163, #165, #167 added embeddings,
responses, passthrough, rerank, images). The growth in
parallelism load wasn't matched by a budget bump.
Fix: raise the deadline to 10s. This:
- Eliminates the recurring rerun churn on feature PRs
- Preserves the "fail loudly on a genuinely stuck snapshot"
property — 10s is still a generous floor; a real bug where
propagation hangs indefinitely would still fail clearly
- Doesn't change the happy-path latency at all (the helper
polls every 50ms and returns as soon as the condition is
met — bumping the deadline only affects the sad path)
This is a test-infra-only change; no product behavior is affected.
The ≤500ms spec target for in-process propagation is unchanged;
this is purely the CI test harness's wait budget for slow runners
under concurrent load.
If 10s proves insufficient as the suite grows further, the next
escalation would be to reduce `maxForks` from 4 to 2 (slower wall
time, less etcd pressure) — tracked in #157 as a fallback.
moonming added a commit that referenced this pull request May 9, 2026
… (#169)
* test(harness): raise waitConfigPropagation budget 5s → 10s (#157)
Closes#157 (test-infra concern, not a product bug).
Issue: the e2e suite's `guardrail-keyword-e2e.test.ts` has flaked
three times on CI in the past 24 hours (#157 first occurrence,
plus reruns required on PR #165 and #167). Failure mode is the
same: `waitConfigPropagation: condition not met within 5s`.
Root cause: vitest is configured with `maxForks: 4` (per
`vitest.config.ts`), so up to 4 test files run in parallel, each
spawning its own `aisix` binary against a SHARED etcd. Each
binary opens watches and writes resources via the admin API
concurrently with the others. Under that load, etcd watch
dispatch latency for the LAST resource in a multi-resource
batch (e.g. a Guardrail rule following Model + ApiKey +
ProviderKey writes) can exceed the 5s budget.
The 5s budget was sized when the suite had ~9 files. The suite
is now 20+ files (#161, #163, #165, #167 added embeddings,
responses, passthrough, rerank, images). The growth in
parallelism load wasn't matched by a budget bump.
Fix: raise the deadline to 10s. This:
- Eliminates the recurring rerun churn on feature PRs
- Preserves the "fail loudly on a genuinely stuck snapshot"
property — 10s is still a generous floor; a real bug where
propagation hangs indefinitely would still fail clearly
- Doesn't change the happy-path latency at all (the helper
polls every 50ms and returns as soon as the condition is
met — bumping the deadline only affects the sad path)
This is a test-infra-only change; no product behavior is affected.
The ≤500ms spec target for in-process propagation is unchanged;
this is purely the CI test harness's wait budget for slow runners
under concurrent load.
If 10s proves insufficient as the suite grows further, the next
escalation would be to reduce `maxForks` from 4 to 2 (slower wall
time, less etcd pressure) — tracked in #157 as a fallback.
* test(harness): also reduce maxForks 4→2 (#157 fallback escalation)
After the timeout bump landed, #169's own CI run still flaked with
the same `condition not met within 10s` error — meaning etcd watch
dispatch latency genuinely exceeds 10s under maxForks=4 with the
current suite size (20+ files). The audit on #169 had flagged this
as the "product-side hypothesis still open" — confirmed.
Apply the documented fallback from #157: cut maxForks from 4 to 2.
This halves the concurrent-watcher count against the shared etcd
and brings dispatch latency back inside the budget.
Trade-off: wall time grows ~1.5-2× per CI run (locally measured
~14s @ maxForks=4 vs ~30s @ maxForks=2, so faster than expected
because the extra parallelism wasn't fully utilized anyway —
contention dominated). Net for CI is "predictable green" vs
"fast but constantly-rerunning".
The 10s `waitConfigPropagation` budget from the prior commit stays
in place as belt-and-suspenders — even with maxForks=2 the original
5s would still be tight on slow runners.
Combined this PR now does:
1. waitConfigPropagation deadline 5s → 10s (`harness/admin.ts`)
2. maxForks 4 → 2 (`vitest.config.ts`)
The product-side hypothesis ("etcd watch dispatch degrades
non-linearly with concurrent watchers") is now strongly supported
and worth a separate product-side investigation tracked in #157
follow-up.
moonming added a commit that referenced this pull request May 10, 2026
#166) (#209)
## Problem
Per `docs/api-proxy.md` §4.10, Anthropic passthrough was supposed to
inject the documented Anthropic auth shape — `x-api-key` +
`anthropic-version` — and only that. In practice the gateway was
ALSO injecting `Authorization: Bearer <provider-secret>` alongside:
```
x-api-key: sk-ant-mock ← correct per Anthropic spec
anthropic-version: 2023-06-01 ← correct per Anthropic spec
Authorization: Bearer sk-ant-mock ← redundant, not in Anthropic's auth contract
```
Real Anthropic ignores the redundant Bearer today, but:
- Future stricter Anthropic gateways or customer-side middleware
could reject the request based on having unexpected auth headers.
- Operators inspecting upstream traffic captures generate
"is this a leak?" tickets every time the redundant header
surfaces — even though the value is the gateway's own provider
secret, not the caller's plaintext bearer.
Not a credential leak (verified by #165's existing assertion that
the caller's plaintext bearer never reaches the upstream), but a
docs-vs-behavior gap that operators shouldn't have to investigate.
## Fix
In `crates/aisix-proxy/src/passthrough.rs::dispatch`, restructure the
auth-injection branch so each provider gets EXACTLY ONE auth shape:
- openai / gemini / deepseek / unknown providers → `Authorization: Bearer …`
- anthropic → `x-api-key` + `anthropic-version` ONLY
- empty `api_key` → no auth headers at all (was previously: still
injected an empty `x-api-key` for anthropic — also cleaned up)
```rust
if api_key.is_empty() {
// nothing to inject
} else if provider_lower == "anthropic" {
builder = builder.header("x-api-key", &api_key);
builder = builder.header("anthropic-version", "2023-06-01");
} else {
builder = builder.header(header::AUTHORIZATION, format!("Bearer {api_key}"));
}
```
## Tests
**Rust unit (`crates/aisix-proxy/src/passthrough.rs`)** — new
`anthropic_passthrough_does_not_inject_bearer_auth` test:
- Configures Anthropic passthrough with secret `sk-ant-test`
- Posts to `/passthrough/anthropic/v1/messages/batches`
- wiremock subset-match on `x-api-key` + `anthropic-version`
- **Strict assertion**: drains `received_requests` and asserts
`!headers.contains_key("authorization")` — the redundant Bearer
must NOT be on the wire
- Sanity: positive value-pin on `x-api-key: sk-ant-test` and
`anthropic-version: 2023-06-01`
**E2E (`tests/e2e/src/cases/passthrough-e2e.test.ts`)** — tightened
the existing Anthropic case from the lenient
`if (auth !== undefined) expect(auth).not.toContain(CALLER_PLAINTEXT)`
to the strict `expect(headers["authorization"]).toBeUndefined()`.
The lenient version caught caller-plaintext leaks (covered by #165);
the strict version catches the wire-cleanliness regression #166
fixes.
Helpers added: `anthropic_model()` and
`anthropic_provider_key_entry()` for the Anthropic-shaped fixtures
(parallel to the existing `openai_model` / `provider_key_entry`).
## Verification
- `cargo test -p aisix-proxy --lib`: **152/152 passing** (incl. new test)
- `cargo clippy -p aisix-proxy --lib --tests -- -D warnings`: clean
- `cargo fmt --check`: clean
- `pnpm tsc --noEmit` (e2e): clean
## References
- Issue: #166
- `docs/api-proxy.md` §4.10 (passthrough contract)
- Anthropic auth spec: <https://docs.anthropic.com/en/api/getting-started>
Sign up for freeto 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.

2 participants

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

test(e2e): C7 /passthrough/anthropic auth-shape switching + verbatim forward - #165

Merged
moonming merged 2 commits into
mainfrom
test/e2e-c7-passthrough
May 9, 2026
Merged

test(e2e): C7 /passthrough/anthropic auth-shape switching + verbatim forward#165
moonming merged 2 commits into
mainfrom
test/e2e-c7-passthrough

Conversation

@moonming

@moonmingmoonming commented May 9, 2026

Copy link
Copy Markdown
Member

Summary

Third endpoint covered from #151's C7 row. Per gateway docs §4.10, `/passthrough/{provider}/*rest` is the lowest-overhead escape hatch for provider endpoints aisix doesn't natively wrap (OpenAI batches / files / fine-tuning, Anthropic message-batches). Prior to this PR the gateway had zero e2e coverage on `/passthrough`.

One of two originally-planned cases ships here. The other surfaced a docs/behavior conflict and is held back as #164.

What's pinned

CaseUser journeyAsserts
Anthropic passthroughPOST `/passthrough/anthropic/v1/messages/batches` with arbitrary bodyCaller sees upstream's response back; upstream got POST at `/v1/messages/batches` (path verbatim after `/passthrough/anthropic` stripped); auth-shape switching: `x-api-key: sk-ant-mock` (NOT caller's bearer) + `anthropic-version: 2023-06-01` (Anthropic's documented current version); upstream Authorization (if present) does not contain caller's plaintext bearer (no credential leak); body forwarded byte-for-byte

Held back as separate issues

🐛 #164 — `/passthrough/openai/*` doubles `/v1` prefix when api_base ends with `/v1` (contradicts published docs). `docs/api-admin.md` §4.3 publishes `api_base: "https://api.openai.com/v1"\`; `docs/api-proxy.md` §4.10 publishes the call as `/passthrough/openai/v1/batches`. Together: `/v1/v1/batches` upstream. OpenAI passthrough test will return once reconciled.

🐛 #166 — `/passthrough/anthropic` injects redundant `Authorization: Bearer` alongside x-api-key. NOT a credential leak (Bearer value is gateway's own provider secret, not caller's plaintext) but a wire-cleanliness gap vs docs §4.10 implication of x-api-key + anthropic-version only for Anthropic. Test pins the leak concern explicitly; the strict "Authorization absent" assertion is the verification once #166 is resolved.

Why this matters

Three real-world failure modes that prior coverage couldn't catch:

  • Auth-shape regression — a regression that forwarded `Authorization: Bearer` to Anthropic upstream as the only auth header would 401 in production.
  • Caller's bearer leak — the test verifies that even when Authorization IS present (per bug: /passthrough/anthropic injects redundant Authorization: Bearer <provider-secret> alongside x-api-key #166's wire-cleanliness gap), it does NOT contain the caller's plaintext bearer.
  • Body normalization regression — passthrough's whole point is "we don't normalize". A regression that JSON-round-tripped or schema-validated bodies on the way through would silently drop unknown fields, breaking batches/files use cases that depend on free-form bodies.

Source-blind discipline

Every assertion derives from external contracts:

No internal Rust paths or struct field names referenced.

Independent audit

Per CLAUDE.md §8, an independent audit agent reviewed the initial commit (4ffd636). Resolution log:

FindingSeverityResolution
Caller-bearer-leak assertion missing despite being a stated motivationMEDIUMTightened: upstream Authorization (if present) must not contain caller's plaintext bearer (7a2ed99)
`anthropic-version` pinned only as "non-empty string" — would pass on malformed values like `"v1"`MEDIUMPinned exact value `"2023-06-01"` per Anthropic docs (7a2ed99)
HTTP method matrix coverage gap (route is `ANY`; only POST tested)MEDIUMAcknowledged scope deferral
Provider matrix coverage gap (only anthropic; gemini/deepseek not tested)MEDIUMAcknowledged scope deferral; OpenAI held back as #164
Body verbatim assertion is string-equalityMEDIUMDeliberate strict contract per docs §4.10 — no change
L1-L3 informationalLOWNot addressed

The audit's recommended strict assertion (`Authorization === undefined`) revealed a separate gateway-side issue (#166): NOT a caller-bearer leak, but a redundant Bearer injection alongside x-api-key. The shipped assertion pins the leak concern explicitly while #166 tracks the wire-cleanliness gap.

Test plan

  • `npm test` (full e2e suite) — 33/33 passing locally (was 32)
  • No mock-data-only paths: case exercises the real `aisix` binary, real etcd config propagation, real fetch reverse-call against `/passthrough/anthropic/...`
  • CI green

Refs #151.

…forward (#151)
Third endpoint covered from #151's C7 row. Per gateway docs §4.10,
/passthrough/{provider}/*rest is the lowest-overhead escape hatch
for provider endpoints aisix doesn't natively wrap (OpenAI batches /
files / fine-tuning, Anthropic message-batches, etc.). Prior to
this file the gateway had **zero** e2e coverage on /passthrough —
every customer using batches/files had no regression protection on
the wire.
Case shipped:
Anthropic passthrough: caller POSTs
/passthrough/anthropic/v1/messages/batches with arbitrary body.
Gateway must:
- Strip /passthrough/anthropic prefix and forward the rest
(`/v1/messages/batches`) verbatim to upstream
- Inject Anthropic's auth shape: `x-api-key` + `anthropic-version`
headers (NOT `Authorization: Bearer` — a regression that
forwarded Bearer to Anthropic upstream would 401 in production
but pass against a permissive mock; the explicit auth header
pin is the only line of defence)
- Forward request body byte-for-byte (the entire point of
passthrough is "we don't normalize the body")
- Round-trip the upstream's response shape to the caller
Case held back: OpenAI passthrough surfaced a documented contract
conflict between docs/api-admin.md §4.3 (api_base example
`https://api.openai.com/v1`) and docs/api-proxy.md §4.10 (passthrough
example `/passthrough/openai/v1/batches`) — together they produce
double-/v1 upstream URL. Filed as #164. Same hold-back pattern as
#153 / #154 / #159 / #162.
References:
- Gateway's own /passthrough contract: `docs/api-proxy.md` §4.10
- Anthropic auth headers spec
<https://docs.anthropic.com/en/api/getting-started>
Refs #151
CopilotAI review requested due to automatic review settings May 9, 2026 13:58
@coderabbitai

coderabbitaiBot commented May 9, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@moonming has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 9 minutes and 11 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: ef642733-c724-4207-9d3a-6db4fe2bd416

📥 Commits

Reviewing files that changed from the base of the PR and between b691bf7 and 7a2ed99.

📒 Files selected for processing (1)
  • tests/e2e/src/cases/passthrough-e2e.test.ts

Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

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

CopilotAI 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.

Pull request overview

Adds end-to-end coverage for the gateway’s /passthrough/{provider}/*rest escape hatch, specifically pinning Anthropic passthrough behavior to prevent auth-shape and wire-format regressions.

Changes:

  • Introduces a new e2e test covering POST /passthrough/anthropic/v1/messages/batches.
  • Asserts passthrough path stripping (upstream sees /v1/messages/batches) and verbatim request-body forwarding.
  • Asserts Anthropic-specific auth-shape switching (x-api-key + non-empty anthropic-version) for upstream requests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

expect(typeof testCalls[0]?.headers["anthropic-version"]).toBe("string");
expect(
(testCalls[0]?.headers["anthropic-version"] as string).length,
).toBeGreaterThan(0);
Audit (per CLAUDE.md §8) found two MEDIUM:
MEDIUM M1 (caller-bearer-leak assertion missing despite being one
of the PR's stated motivations):
Initial PR's assertions verified `x-api-key: sk-ant-mock` correct
injection but did NOT pin that the caller's plaintext bearer was
absent on the upstream side — a regression that injected
x-api-key correctly AND forwarded the caller's Bearer would
silently pass.
Audit recommended `expect(authorization).toBeUndefined()`. That
assertion FAILED (gateway today sends `Authorization: Bearer
sk-ant-mock` redundantly alongside x-api-key) — surfacing a
separate issue: NOT a caller-bearer leak (the value is the
gateway's own provider secret, not the caller's plaintext), but
a wire-cleanliness gap vs docs §4.10 which implies x-api-key +
anthropic-version only for Anthropic. Filed separately as #166.
Settled assertion: pin the LEAK concern explicitly — if the
upstream Authorization is present, its value must NOT contain
the caller's plaintext bearer. Catches the regression mode the
PR description claimed to defend against.
MEDIUM M2 (anthropic-version pinned only as "non-empty string"):
Pinned exact value `"2023-06-01"` per Anthropic's documented
current API version
(<https://docs.anthropic.com/en/api/getting-started>). A
regression that injected a malformed-but-non-empty value (e.g.
"v1", "latest", "unspecified") would 400 against real Anthropic
but pass against the permissive mock without this exact pin.
MEDIUM M3-M4 (HTTP method matrix, provider matrix gaps):
Acknowledged scope deferrals; not addressed in this PR. Worth
filing as separate follow-up.
LOW L1-L3: not addressed.
All 33 e2e tests still pass locally.
@moonming
moonming merged commit 971d119 into mainMay 9, 2026
10 of 11 checks passed
@moonming
moonming deleted the test/e2e-c7-passthrough branch May 9, 2026 14:15
moonming added a commit that referenced this pull request May 9, 2026
Closes#157 (test-infra concern, not a product bug).
Issue: the e2e suite's `guardrail-keyword-e2e.test.ts` has flaked
three times on CI in the past 24 hours (#157 first occurrence,
plus reruns required on PR #165 and #167). Failure mode is the
same: `waitConfigPropagation: condition not met within 5s`.
Root cause: vitest is configured with `maxForks: 4` (per
`vitest.config.ts`), so up to 4 test files run in parallel, each
spawning its own `aisix` binary against a SHARED etcd. Each
binary opens watches and writes resources via the admin API
concurrently with the others. Under that load, etcd watch
dispatch latency for the LAST resource in a multi-resource
batch (e.g. a Guardrail rule following Model + ApiKey +
ProviderKey writes) can exceed the 5s budget.
The 5s budget was sized when the suite had ~9 files. The suite
is now 20+ files (#161, #163, #165, #167 added embeddings,
responses, passthrough, rerank, images). The growth in
parallelism load wasn't matched by a budget bump.
Fix: raise the deadline to 10s. This:
- Eliminates the recurring rerun churn on feature PRs
- Preserves the "fail loudly on a genuinely stuck snapshot"
property — 10s is still a generous floor; a real bug where
propagation hangs indefinitely would still fail clearly
- Doesn't change the happy-path latency at all (the helper
polls every 50ms and returns as soon as the condition is
met — bumping the deadline only affects the sad path)
This is a test-infra-only change; no product behavior is affected.
The ≤500ms spec target for in-process propagation is unchanged;
this is purely the CI test harness's wait budget for slow runners
under concurrent load.
If 10s proves insufficient as the suite grows further, the next
escalation would be to reduce `maxForks` from 4 to 2 (slower wall
time, less etcd pressure) — tracked in #157 as a fallback.
moonming added a commit that referenced this pull request May 9, 2026
… (#169)
* test(harness): raise waitConfigPropagation budget 5s → 10s (#157)
Closes#157 (test-infra concern, not a product bug).
Issue: the e2e suite's `guardrail-keyword-e2e.test.ts` has flaked
three times on CI in the past 24 hours (#157 first occurrence,
plus reruns required on PR #165 and #167). Failure mode is the
same: `waitConfigPropagation: condition not met within 5s`.
Root cause: vitest is configured with `maxForks: 4` (per
`vitest.config.ts`), so up to 4 test files run in parallel, each
spawning its own `aisix` binary against a SHARED etcd. Each
binary opens watches and writes resources via the admin API
concurrently with the others. Under that load, etcd watch
dispatch latency for the LAST resource in a multi-resource
batch (e.g. a Guardrail rule following Model + ApiKey +
ProviderKey writes) can exceed the 5s budget.
The 5s budget was sized when the suite had ~9 files. The suite
is now 20+ files (#161, #163, #165, #167 added embeddings,
responses, passthrough, rerank, images). The growth in
parallelism load wasn't matched by a budget bump.
Fix: raise the deadline to 10s. This:
- Eliminates the recurring rerun churn on feature PRs
- Preserves the "fail loudly on a genuinely stuck snapshot"
property — 10s is still a generous floor; a real bug where
propagation hangs indefinitely would still fail clearly
- Doesn't change the happy-path latency at all (the helper
polls every 50ms and returns as soon as the condition is
met — bumping the deadline only affects the sad path)
This is a test-infra-only change; no product behavior is affected.
The ≤500ms spec target for in-process propagation is unchanged;
this is purely the CI test harness's wait budget for slow runners
under concurrent load.
If 10s proves insufficient as the suite grows further, the next
escalation would be to reduce `maxForks` from 4 to 2 (slower wall
time, less etcd pressure) — tracked in #157 as a fallback.
* test(harness): also reduce maxForks 4→2 (#157 fallback escalation)
After the timeout bump landed, #169's own CI run still flaked with
the same `condition not met within 10s` error — meaning etcd watch
dispatch latency genuinely exceeds 10s under maxForks=4 with the
current suite size (20+ files). The audit on #169 had flagged this
as the "product-side hypothesis still open" — confirmed.
Apply the documented fallback from #157: cut maxForks from 4 to 2.
This halves the concurrent-watcher count against the shared etcd
and brings dispatch latency back inside the budget.
Trade-off: wall time grows ~1.5-2× per CI run (locally measured
~14s @ maxForks=4 vs ~30s @ maxForks=2, so faster than expected
because the extra parallelism wasn't fully utilized anyway —
contention dominated). Net for CI is "predictable green" vs
"fast but constantly-rerunning".
The 10s `waitConfigPropagation` budget from the prior commit stays
in place as belt-and-suspenders — even with maxForks=2 the original
5s would still be tight on slow runners.
Combined this PR now does:
1. waitConfigPropagation deadline 5s → 10s (`harness/admin.ts`)
2. maxForks 4 → 2 (`vitest.config.ts`)
The product-side hypothesis ("etcd watch dispatch degrades
non-linearly with concurrent watchers") is now strongly supported
and worth a separate product-side investigation tracked in #157
follow-up.
moonming added a commit that referenced this pull request May 10, 2026
#166) (#209)
## Problem
Per `docs/api-proxy.md` §4.10, Anthropic passthrough was supposed to
inject the documented Anthropic auth shape — `x-api-key` +
`anthropic-version` — and only that. In practice the gateway was
ALSO injecting `Authorization: Bearer <provider-secret>` alongside:
```
x-api-key: sk-ant-mock ← correct per Anthropic spec
anthropic-version: 2023-06-01 ← correct per Anthropic spec
Authorization: Bearer sk-ant-mock ← redundant, not in Anthropic's auth contract
```
Real Anthropic ignores the redundant Bearer today, but:
- Future stricter Anthropic gateways or customer-side middleware
could reject the request based on having unexpected auth headers.
- Operators inspecting upstream traffic captures generate
"is this a leak?" tickets every time the redundant header
surfaces — even though the value is the gateway's own provider
secret, not the caller's plaintext bearer.
Not a credential leak (verified by #165's existing assertion that
the caller's plaintext bearer never reaches the upstream), but a
docs-vs-behavior gap that operators shouldn't have to investigate.
## Fix
In `crates/aisix-proxy/src/passthrough.rs::dispatch`, restructure the
auth-injection branch so each provider gets EXACTLY ONE auth shape:
- openai / gemini / deepseek / unknown providers → `Authorization: Bearer …`
- anthropic → `x-api-key` + `anthropic-version` ONLY
- empty `api_key` → no auth headers at all (was previously: still
injected an empty `x-api-key` for anthropic — also cleaned up)
```rust
if api_key.is_empty() {
// nothing to inject
} else if provider_lower == "anthropic" {
builder = builder.header("x-api-key", &api_key);
builder = builder.header("anthropic-version", "2023-06-01");
} else {
builder = builder.header(header::AUTHORIZATION, format!("Bearer {api_key}"));
}
```
## Tests
**Rust unit (`crates/aisix-proxy/src/passthrough.rs`)** — new
`anthropic_passthrough_does_not_inject_bearer_auth` test:
- Configures Anthropic passthrough with secret `sk-ant-test`
- Posts to `/passthrough/anthropic/v1/messages/batches`
- wiremock subset-match on `x-api-key` + `anthropic-version`
- **Strict assertion**: drains `received_requests` and asserts
`!headers.contains_key("authorization")` — the redundant Bearer
must NOT be on the wire
- Sanity: positive value-pin on `x-api-key: sk-ant-test` and
`anthropic-version: 2023-06-01`
**E2E (`tests/e2e/src/cases/passthrough-e2e.test.ts`)** — tightened
the existing Anthropic case from the lenient
`if (auth !== undefined) expect(auth).not.toContain(CALLER_PLAINTEXT)`
to the strict `expect(headers["authorization"]).toBeUndefined()`.
The lenient version caught caller-plaintext leaks (covered by #165);
the strict version catches the wire-cleanliness regression #166
fixes.
Helpers added: `anthropic_model()` and
`anthropic_provider_key_entry()` for the Anthropic-shaped fixtures
(parallel to the existing `openai_model` / `provider_key_entry`).
## Verification
- `cargo test -p aisix-proxy --lib`: **152/152 passing** (incl. new test)
- `cargo clippy -p aisix-proxy --lib --tests -- -D warnings`: clean
- `cargo fmt --check`: clean
- `pnpm tsc --noEmit` (e2e): clean
## References
- Issue: #166
- `docs/api-proxy.md` §4.10 (passthrough contract)
- Anthropic auth spec: <https://docs.anthropic.com/en/api/getting-started>
Sign up for freeto 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.

2 participants

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

test(e2e): C7 /passthrough/anthropic auth-shape switching + verbatim forward - #165

Merged
moonming merged 2 commits into
mainfrom
test/e2e-c7-passthrough
May 9, 2026
Merged

test(e2e): C7 /passthrough/anthropic auth-shape switching + verbatim forward#165
moonming merged 2 commits into
mainfrom
test/e2e-c7-passthrough

Conversation

@moonming

@moonmingmoonming commented May 9, 2026

Copy link
Copy Markdown
Member

Summary

Third endpoint covered from #151's C7 row. Per gateway docs §4.10, `/passthrough/{provider}/*rest` is the lowest-overhead escape hatch for provider endpoints aisix doesn't natively wrap (OpenAI batches / files / fine-tuning, Anthropic message-batches). Prior to this PR the gateway had zero e2e coverage on `/passthrough`.

One of two originally-planned cases ships here. The other surfaced a docs/behavior conflict and is held back as #164.

What's pinned

CaseUser journeyAsserts
Anthropic passthroughPOST `/passthrough/anthropic/v1/messages/batches` with arbitrary bodyCaller sees upstream's response back; upstream got POST at `/v1/messages/batches` (path verbatim after `/passthrough/anthropic` stripped); auth-shape switching: `x-api-key: sk-ant-mock` (NOT caller's bearer) + `anthropic-version: 2023-06-01` (Anthropic's documented current version); upstream Authorization (if present) does not contain caller's plaintext bearer (no credential leak); body forwarded byte-for-byte

Held back as separate issues

🐛 #164 — `/passthrough/openai/*` doubles `/v1` prefix when api_base ends with `/v1` (contradicts published docs). `docs/api-admin.md` §4.3 publishes `api_base: "https://api.openai.com/v1"\`; `docs/api-proxy.md` §4.10 publishes the call as `/passthrough/openai/v1/batches`. Together: `/v1/v1/batches` upstream. OpenAI passthrough test will return once reconciled.

🐛 #166 — `/passthrough/anthropic` injects redundant `Authorization: Bearer` alongside x-api-key. NOT a credential leak (Bearer value is gateway's own provider secret, not caller's plaintext) but a wire-cleanliness gap vs docs §4.10 implication of x-api-key + anthropic-version only for Anthropic. Test pins the leak concern explicitly; the strict "Authorization absent" assertion is the verification once #166 is resolved.

Why this matters

Three real-world failure modes that prior coverage couldn't catch:

  • Auth-shape regression — a regression that forwarded `Authorization: Bearer` to Anthropic upstream as the only auth header would 401 in production.
  • Caller's bearer leak — the test verifies that even when Authorization IS present (per bug: /passthrough/anthropic injects redundant Authorization: Bearer <provider-secret> alongside x-api-key #166's wire-cleanliness gap), it does NOT contain the caller's plaintext bearer.
  • Body normalization regression — passthrough's whole point is "we don't normalize". A regression that JSON-round-tripped or schema-validated bodies on the way through would silently drop unknown fields, breaking batches/files use cases that depend on free-form bodies.

Source-blind discipline

Every assertion derives from external contracts:

No internal Rust paths or struct field names referenced.

Independent audit

Per CLAUDE.md §8, an independent audit agent reviewed the initial commit (4ffd636). Resolution log:

FindingSeverityResolution
Caller-bearer-leak assertion missing despite being a stated motivationMEDIUMTightened: upstream Authorization (if present) must not contain caller's plaintext bearer (7a2ed99)
`anthropic-version` pinned only as "non-empty string" — would pass on malformed values like `"v1"`MEDIUMPinned exact value `"2023-06-01"` per Anthropic docs (7a2ed99)
HTTP method matrix coverage gap (route is `ANY`; only POST tested)MEDIUMAcknowledged scope deferral
Provider matrix coverage gap (only anthropic; gemini/deepseek not tested)MEDIUMAcknowledged scope deferral; OpenAI held back as #164
Body verbatim assertion is string-equalityMEDIUMDeliberate strict contract per docs §4.10 — no change
L1-L3 informationalLOWNot addressed

The audit's recommended strict assertion (`Authorization === undefined`) revealed a separate gateway-side issue (#166): NOT a caller-bearer leak, but a redundant Bearer injection alongside x-api-key. The shipped assertion pins the leak concern explicitly while #166 tracks the wire-cleanliness gap.

Test plan

  • `npm test` (full e2e suite) — 33/33 passing locally (was 32)
  • No mock-data-only paths: case exercises the real `aisix` binary, real etcd config propagation, real fetch reverse-call against `/passthrough/anthropic/...`
  • CI green

Refs #151.

…forward (#151)
Third endpoint covered from #151's C7 row. Per gateway docs §4.10,
/passthrough/{provider}/*rest is the lowest-overhead escape hatch
for provider endpoints aisix doesn't natively wrap (OpenAI batches /
files / fine-tuning, Anthropic message-batches, etc.). Prior to
this file the gateway had **zero** e2e coverage on /passthrough —
every customer using batches/files had no regression protection on
the wire.
Case shipped:
Anthropic passthrough: caller POSTs
/passthrough/anthropic/v1/messages/batches with arbitrary body.
Gateway must:
- Strip /passthrough/anthropic prefix and forward the rest
(`/v1/messages/batches`) verbatim to upstream
- Inject Anthropic's auth shape: `x-api-key` + `anthropic-version`
headers (NOT `Authorization: Bearer` — a regression that
forwarded Bearer to Anthropic upstream would 401 in production
but pass against a permissive mock; the explicit auth header
pin is the only line of defence)
- Forward request body byte-for-byte (the entire point of
passthrough is "we don't normalize the body")
- Round-trip the upstream's response shape to the caller
Case held back: OpenAI passthrough surfaced a documented contract
conflict between docs/api-admin.md §4.3 (api_base example
`https://api.openai.com/v1`) and docs/api-proxy.md §4.10 (passthrough
example `/passthrough/openai/v1/batches`) — together they produce
double-/v1 upstream URL. Filed as #164. Same hold-back pattern as
#153 / #154 / #159 / #162.
References:
- Gateway's own /passthrough contract: `docs/api-proxy.md` §4.10
- Anthropic auth headers spec
<https://docs.anthropic.com/en/api/getting-started>
Refs #151
CopilotAI review requested due to automatic review settings May 9, 2026 13:58
@coderabbitai

coderabbitaiBot commented May 9, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@moonming has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 9 minutes and 11 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: ef642733-c724-4207-9d3a-6db4fe2bd416

📥 Commits

Reviewing files that changed from the base of the PR and between b691bf7 and 7a2ed99.

📒 Files selected for processing (1)
  • tests/e2e/src/cases/passthrough-e2e.test.ts

Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

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

CopilotAI 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.

Pull request overview

Adds end-to-end coverage for the gateway’s /passthrough/{provider}/*rest escape hatch, specifically pinning Anthropic passthrough behavior to prevent auth-shape and wire-format regressions.

Changes:

  • Introduces a new e2e test covering POST /passthrough/anthropic/v1/messages/batches.
  • Asserts passthrough path stripping (upstream sees /v1/messages/batches) and verbatim request-body forwarding.
  • Asserts Anthropic-specific auth-shape switching (x-api-key + non-empty anthropic-version) for upstream requests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

expect(typeof testCalls[0]?.headers["anthropic-version"]).toBe("string");
expect(
(testCalls[0]?.headers["anthropic-version"] as string).length,
).toBeGreaterThan(0);
Audit (per CLAUDE.md §8) found two MEDIUM:
MEDIUM M1 (caller-bearer-leak assertion missing despite being one
of the PR's stated motivations):
Initial PR's assertions verified `x-api-key: sk-ant-mock` correct
injection but did NOT pin that the caller's plaintext bearer was
absent on the upstream side — a regression that injected
x-api-key correctly AND forwarded the caller's Bearer would
silently pass.
Audit recommended `expect(authorization).toBeUndefined()`. That
assertion FAILED (gateway today sends `Authorization: Bearer
sk-ant-mock` redundantly alongside x-api-key) — surfacing a
separate issue: NOT a caller-bearer leak (the value is the
gateway's own provider secret, not the caller's plaintext), but
a wire-cleanliness gap vs docs §4.10 which implies x-api-key +
anthropic-version only for Anthropic. Filed separately as #166.
Settled assertion: pin the LEAK concern explicitly — if the
upstream Authorization is present, its value must NOT contain
the caller's plaintext bearer. Catches the regression mode the
PR description claimed to defend against.
MEDIUM M2 (anthropic-version pinned only as "non-empty string"):
Pinned exact value `"2023-06-01"` per Anthropic's documented
current API version
(<https://docs.anthropic.com/en/api/getting-started>). A
regression that injected a malformed-but-non-empty value (e.g.
"v1", "latest", "unspecified") would 400 against real Anthropic
but pass against the permissive mock without this exact pin.
MEDIUM M3-M4 (HTTP method matrix, provider matrix gaps):
Acknowledged scope deferrals; not addressed in this PR. Worth
filing as separate follow-up.
LOW L1-L3: not addressed.
All 33 e2e tests still pass locally.
@moonming
moonming merged commit 971d119 into mainMay 9, 2026
10 of 11 checks passed
@moonming
moonming deleted the test/e2e-c7-passthrough branch May 9, 2026 14:15
moonming added a commit that referenced this pull request May 9, 2026
Closes#157 (test-infra concern, not a product bug).
Issue: the e2e suite's `guardrail-keyword-e2e.test.ts` has flaked
three times on CI in the past 24 hours (#157 first occurrence,
plus reruns required on PR #165 and #167). Failure mode is the
same: `waitConfigPropagation: condition not met within 5s`.
Root cause: vitest is configured with `maxForks: 4` (per
`vitest.config.ts`), so up to 4 test files run in parallel, each
spawning its own `aisix` binary against a SHARED etcd. Each
binary opens watches and writes resources via the admin API
concurrently with the others. Under that load, etcd watch
dispatch latency for the LAST resource in a multi-resource
batch (e.g. a Guardrail rule following Model + ApiKey +
ProviderKey writes) can exceed the 5s budget.
The 5s budget was sized when the suite had ~9 files. The suite
is now 20+ files (#161, #163, #165, #167 added embeddings,
responses, passthrough, rerank, images). The growth in
parallelism load wasn't matched by a budget bump.
Fix: raise the deadline to 10s. This:
- Eliminates the recurring rerun churn on feature PRs
- Preserves the "fail loudly on a genuinely stuck snapshot"
property — 10s is still a generous floor; a real bug where
propagation hangs indefinitely would still fail clearly
- Doesn't change the happy-path latency at all (the helper
polls every 50ms and returns as soon as the condition is
met — bumping the deadline only affects the sad path)
This is a test-infra-only change; no product behavior is affected.
The ≤500ms spec target for in-process propagation is unchanged;
this is purely the CI test harness's wait budget for slow runners
under concurrent load.
If 10s proves insufficient as the suite grows further, the next
escalation would be to reduce `maxForks` from 4 to 2 (slower wall
time, less etcd pressure) — tracked in #157 as a fallback.
moonming added a commit that referenced this pull request May 9, 2026
… (#169)
* test(harness): raise waitConfigPropagation budget 5s → 10s (#157)
Closes#157 (test-infra concern, not a product bug).
Issue: the e2e suite's `guardrail-keyword-e2e.test.ts` has flaked
three times on CI in the past 24 hours (#157 first occurrence,
plus reruns required on PR #165 and #167). Failure mode is the
same: `waitConfigPropagation: condition not met within 5s`.
Root cause: vitest is configured with `maxForks: 4` (per
`vitest.config.ts`), so up to 4 test files run in parallel, each
spawning its own `aisix` binary against a SHARED etcd. Each
binary opens watches and writes resources via the admin API
concurrently with the others. Under that load, etcd watch
dispatch latency for the LAST resource in a multi-resource
batch (e.g. a Guardrail rule following Model + ApiKey +
ProviderKey writes) can exceed the 5s budget.
The 5s budget was sized when the suite had ~9 files. The suite
is now 20+ files (#161, #163, #165, #167 added embeddings,
responses, passthrough, rerank, images). The growth in
parallelism load wasn't matched by a budget bump.
Fix: raise the deadline to 10s. This:
- Eliminates the recurring rerun churn on feature PRs
- Preserves the "fail loudly on a genuinely stuck snapshot"
property — 10s is still a generous floor; a real bug where
propagation hangs indefinitely would still fail clearly
- Doesn't change the happy-path latency at all (the helper
polls every 50ms and returns as soon as the condition is
met — bumping the deadline only affects the sad path)
This is a test-infra-only change; no product behavior is affected.
The ≤500ms spec target for in-process propagation is unchanged;
this is purely the CI test harness's wait budget for slow runners
under concurrent load.
If 10s proves insufficient as the suite grows further, the next
escalation would be to reduce `maxForks` from 4 to 2 (slower wall
time, less etcd pressure) — tracked in #157 as a fallback.
* test(harness): also reduce maxForks 4→2 (#157 fallback escalation)
After the timeout bump landed, #169's own CI run still flaked with
the same `condition not met within 10s` error — meaning etcd watch
dispatch latency genuinely exceeds 10s under maxForks=4 with the
current suite size (20+ files). The audit on #169 had flagged this
as the "product-side hypothesis still open" — confirmed.
Apply the documented fallback from #157: cut maxForks from 4 to 2.
This halves the concurrent-watcher count against the shared etcd
and brings dispatch latency back inside the budget.
Trade-off: wall time grows ~1.5-2× per CI run (locally measured
~14s @ maxForks=4 vs ~30s @ maxForks=2, so faster than expected
because the extra parallelism wasn't fully utilized anyway —
contention dominated). Net for CI is "predictable green" vs
"fast but constantly-rerunning".
The 10s `waitConfigPropagation` budget from the prior commit stays
in place as belt-and-suspenders — even with maxForks=2 the original
5s would still be tight on slow runners.
Combined this PR now does:
1. waitConfigPropagation deadline 5s → 10s (`harness/admin.ts`)
2. maxForks 4 → 2 (`vitest.config.ts`)
The product-side hypothesis ("etcd watch dispatch degrades
non-linearly with concurrent watchers") is now strongly supported
and worth a separate product-side investigation tracked in #157
follow-up.
moonming added a commit that referenced this pull request May 10, 2026
#166) (#209)
## Problem
Per `docs/api-proxy.md` §4.10, Anthropic passthrough was supposed to
inject the documented Anthropic auth shape — `x-api-key` +
`anthropic-version` — and only that. In practice the gateway was
ALSO injecting `Authorization: Bearer <provider-secret>` alongside:
```
x-api-key: sk-ant-mock ← correct per Anthropic spec
anthropic-version: 2023-06-01 ← correct per Anthropic spec
Authorization: Bearer sk-ant-mock ← redundant, not in Anthropic's auth contract
```
Real Anthropic ignores the redundant Bearer today, but:
- Future stricter Anthropic gateways or customer-side middleware
could reject the request based on having unexpected auth headers.
- Operators inspecting upstream traffic captures generate
"is this a leak?" tickets every time the redundant header
surfaces — even though the value is the gateway's own provider
secret, not the caller's plaintext bearer.
Not a credential leak (verified by #165's existing assertion that
the caller's plaintext bearer never reaches the upstream), but a
docs-vs-behavior gap that operators shouldn't have to investigate.
## Fix
In `crates/aisix-proxy/src/passthrough.rs::dispatch`, restructure the
auth-injection branch so each provider gets EXACTLY ONE auth shape:
- openai / gemini / deepseek / unknown providers → `Authorization: Bearer …`
- anthropic → `x-api-key` + `anthropic-version` ONLY
- empty `api_key` → no auth headers at all (was previously: still
injected an empty `x-api-key` for anthropic — also cleaned up)
```rust
if api_key.is_empty() {
// nothing to inject
} else if provider_lower == "anthropic" {
builder = builder.header("x-api-key", &api_key);
builder = builder.header("anthropic-version", "2023-06-01");
} else {
builder = builder.header(header::AUTHORIZATION, format!("Bearer {api_key}"));
}
```
## Tests
**Rust unit (`crates/aisix-proxy/src/passthrough.rs`)** — new
`anthropic_passthrough_does_not_inject_bearer_auth` test:
- Configures Anthropic passthrough with secret `sk-ant-test`
- Posts to `/passthrough/anthropic/v1/messages/batches`
- wiremock subset-match on `x-api-key` + `anthropic-version`
- **Strict assertion**: drains `received_requests` and asserts
`!headers.contains_key("authorization")` — the redundant Bearer
must NOT be on the wire
- Sanity: positive value-pin on `x-api-key: sk-ant-test` and
`anthropic-version: 2023-06-01`
**E2E (`tests/e2e/src/cases/passthrough-e2e.test.ts`)** — tightened
the existing Anthropic case from the lenient
`if (auth !== undefined) expect(auth).not.toContain(CALLER_PLAINTEXT)`
to the strict `expect(headers["authorization"]).toBeUndefined()`.
The lenient version caught caller-plaintext leaks (covered by #165);
the strict version catches the wire-cleanliness regression #166
fixes.
Helpers added: `anthropic_model()` and
`anthropic_provider_key_entry()` for the Anthropic-shaped fixtures
(parallel to the existing `openai_model` / `provider_key_entry`).
## Verification
- `cargo test -p aisix-proxy --lib`: **152/152 passing** (incl. new test)
- `cargo clippy -p aisix-proxy --lib --tests -- -D warnings`: clean
- `cargo fmt --check`: clean
- `pnpm tsc --noEmit` (e2e): clean
## References
- Issue: #166
- `docs/api-proxy.md` §4.10 (passthrough contract)
- Anthropic auth spec: <https://docs.anthropic.com/en/api/getting-started>
Sign up for freeto 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.

2 participants

@moonming