mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-06 23:32:12 +03:00
fix(executor): guard claude/anthropic buildHeaders against empty credentials and extend dual-Bearer parity for third-party baseUrls (#8653)
This commit is contained in:
committed by
GitHub
parent
37edd74f2d
commit
eaea0347ac
1
changelog.d/fixes/8653-fix.plan.md
Normal file
1
changelog.d/fixes/8653-fix.plan.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(executor): guard claude/anthropic buildHeaders against empty credentials and extend dual-Bearer parity for third-party baseUrls (#8653)
|
||||
@@ -395,9 +395,26 @@ export class DefaultExecutor extends BaseExecutor {
|
||||
}
|
||||
case "claude":
|
||||
case "anthropic":
|
||||
effectiveKey
|
||||
? (headers["x-api-key"] = effectiveKey)
|
||||
: (headers["Authorization"] = `Bearer ${credentials.accessToken}`);
|
||||
if (effectiveKey) {
|
||||
headers["x-api-key"] = effectiveKey;
|
||||
// Port of decolua/9router commit b977bf74:
|
||||
// Third-party Anthropic-compatible gateways frequently require
|
||||
// Authorization: Bearer ALONGSIDE x-api-key — without it they
|
||||
// return 401 missing_api_key on every forward. Only emit the
|
||||
// Bearer fallback for non-official upstreams; api.anthropic.com
|
||||
// (and the empty/default baseUrl that targets it) must keep the
|
||||
// x-api-key-only behavior to avoid regressing the official path.
|
||||
const baseUrl = credentials?.providerSpecificData?.baseUrl || "";
|
||||
const isOfficial = isOfficialAnthropicBaseUrl(baseUrl);
|
||||
if (!isOfficial) {
|
||||
headers["Authorization"] = `Bearer ${effectiveKey}`;
|
||||
}
|
||||
} else if (credentials.accessToken) {
|
||||
headers["Authorization"] = `Bearer ${credentials.accessToken}`;
|
||||
}
|
||||
// If neither effectiveKey nor accessToken is available, emit no
|
||||
// auth header — the handler will produce a clean "no credentials"
|
||||
// 4xx instead of forwarding garbage auth headers to the upstream.
|
||||
break;
|
||||
case "glm":
|
||||
case "glmt":
|
||||
|
||||
160
tests/unit/executor-default-anthropic-auth-8653.test.ts
Normal file
160
tests/unit/executor-default-anthropic-auth-8653.test.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* Regression tests for #8653: Claude Code 2.1.220 returns 401 Missing API key
|
||||
*
|
||||
* Root cause: DefaultExecutor.buildHeaders for the built-in `claude`/`anthropic`
|
||||
* providers emitted `Authorization: Bearer null` when the connection has an
|
||||
* empty apiKey and no accessToken, and for `anthropic-compatible-*` nodes omitted
|
||||
* the auth header entirely — both get forwarded to the upstream, producing the
|
||||
* relayed "401 Missing API key" error.
|
||||
*
|
||||
* Fix: Guard against falsy credentials (no garbage headers), and extend the
|
||||
* 9router b977bf74 dual-header fix (Bearer alongside x-api-key) to the built-in
|
||||
* `claude`/`anthropic` providers for non-official baseUrls.
|
||||
*/
|
||||
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { DefaultExecutor } from "../../open-sse/executors/default.ts";
|
||||
|
||||
// ── claude / anthropic — empty credentials guard ─────────────────────────
|
||||
|
||||
test("claude provider with empty apiKey and no accessToken does NOT emit Authorization header", () => {
|
||||
const executor = new DefaultExecutor("claude");
|
||||
const headers = executor.buildHeaders(
|
||||
{ apiKey: "", providerSpecificData: {} } as Record<string, unknown>,
|
||||
true
|
||||
) as Record<string, string>;
|
||||
// Must not emit 'Bearer null' / 'Bearer undefined'
|
||||
assert.equal(headers["Authorization"], undefined);
|
||||
assert.equal(headers["x-api-key"], undefined);
|
||||
});
|
||||
|
||||
test("anthropic provider with empty apiKey and no accessToken does NOT emit Authorization header", () => {
|
||||
const executor = new DefaultExecutor("anthropic");
|
||||
const headers = executor.buildHeaders(
|
||||
{ apiKey: "", providerSpecificData: {} } as Record<string, unknown>,
|
||||
true
|
||||
) as Record<string, string>;
|
||||
assert.equal(headers["Authorization"], undefined);
|
||||
assert.equal(headers["x-api-key"], undefined);
|
||||
});
|
||||
|
||||
test("claude provider with both apiKey and accessToken as null/undefined does NOT emit Bearer null", () => {
|
||||
const executor = new DefaultExecutor("claude");
|
||||
const headers = executor.buildHeaders(
|
||||
{ providerSpecificData: {} } as Record<string, unknown>,
|
||||
true
|
||||
) as Record<string, string>;
|
||||
assert.equal(headers["Authorization"], undefined);
|
||||
assert.equal(headers["x-api-key"], undefined);
|
||||
});
|
||||
|
||||
// ── claude / anthropic — dual-header parity (9router b977bf74) ──────────
|
||||
|
||||
test("claude provider with non-official baseUrl sends BOTH x-api-key and Authorization: Bearer", () => {
|
||||
const executor = new DefaultExecutor("claude");
|
||||
const headers = executor.buildHeaders(
|
||||
{
|
||||
apiKey: "k-third-party",
|
||||
providerSpecificData: { baseUrl: "https://gateway.example/v1" },
|
||||
} as Record<string, unknown>,
|
||||
true
|
||||
) as Record<string, string>;
|
||||
assert.equal(headers["x-api-key"], "k-third-party");
|
||||
assert.equal(
|
||||
headers["Authorization"],
|
||||
"Bearer k-third-party",
|
||||
"third-party claude upstream needs the Bearer fallback alongside x-api-key"
|
||||
);
|
||||
});
|
||||
|
||||
test("anthropic provider with non-official baseUrl sends BOTH x-api-key and Authorization: Bearer", () => {
|
||||
const executor = new DefaultExecutor("anthropic");
|
||||
const headers = executor.buildHeaders(
|
||||
{
|
||||
apiKey: "k-third-party",
|
||||
providerSpecificData: { baseUrl: "https://anthropic-proxy.example/v1" },
|
||||
} as Record<string, unknown>,
|
||||
true
|
||||
) as Record<string, string>;
|
||||
assert.equal(headers["x-api-key"], "k-third-party");
|
||||
assert.equal(
|
||||
headers["Authorization"],
|
||||
"Bearer k-third-party",
|
||||
"third-party anthropic upstream needs the Bearer fallback alongside x-api-key"
|
||||
);
|
||||
});
|
||||
|
||||
// ── claude / anthropic — official api.anthropic.com stays x-api-key-only ─
|
||||
|
||||
test("claude provider with official api.anthropic.com baseUrl: x-api-key only, no Bearer", () => {
|
||||
const executor = new DefaultExecutor("claude");
|
||||
const headers = executor.buildHeaders(
|
||||
{
|
||||
apiKey: "k-official",
|
||||
providerSpecificData: { baseUrl: "https://api.anthropic.com/v1" },
|
||||
} as Record<string, unknown>,
|
||||
true
|
||||
) as Record<string, string>;
|
||||
assert.equal(headers["x-api-key"], "k-official");
|
||||
assert.equal(
|
||||
headers["Authorization"],
|
||||
undefined,
|
||||
"official api.anthropic.com must NOT receive a Bearer header alongside x-api-key"
|
||||
);
|
||||
});
|
||||
|
||||
test("anthropic provider with official api.anthropic.com baseUrl: x-api-key only, no Bearer", () => {
|
||||
const executor = new DefaultExecutor("anthropic");
|
||||
const headers = executor.buildHeaders(
|
||||
{
|
||||
apiKey: "k-official",
|
||||
providerSpecificData: { baseUrl: "https://api.anthropic.com/v1" },
|
||||
} as Record<string, unknown>,
|
||||
true
|
||||
) as Record<string, string>;
|
||||
assert.equal(headers["x-api-key"], "k-official");
|
||||
assert.equal(headers["Authorization"], undefined);
|
||||
});
|
||||
|
||||
test("claude provider with empty baseUrl (defaults to official): x-api-key only, no Bearer", () => {
|
||||
const executor = new DefaultExecutor("claude");
|
||||
const headers = executor.buildHeaders(
|
||||
{ apiKey: "k-empty", providerSpecificData: {} } as Record<string, unknown>,
|
||||
true
|
||||
) as Record<string, string>;
|
||||
assert.equal(headers["x-api-key"], "k-empty");
|
||||
assert.equal(headers["Authorization"], undefined);
|
||||
});
|
||||
|
||||
// ── claude OAuth (accessToken-only) keeps Authorization: Bearer ──────────
|
||||
|
||||
test("claude provider with accessToken-only (OAuth mode): Authorization Bearer, no x-api-key", () => {
|
||||
const executor = new DefaultExecutor("claude");
|
||||
const headers = executor.buildHeaders(
|
||||
{ accessToken: "oauth-token", providerSpecificData: {} } as Record<string, unknown>,
|
||||
true
|
||||
) as Record<string, string>;
|
||||
assert.equal(headers["Authorization"], "Bearer oauth-token");
|
||||
assert.equal(headers["x-api-key"], undefined);
|
||||
});
|
||||
|
||||
test("anthropic provider with accessToken-only (OAuth mode): Authorization Bearer, no x-api-key", () => {
|
||||
const executor = new DefaultExecutor("anthropic");
|
||||
const headers = executor.buildHeaders(
|
||||
{ accessToken: "oauth-token", providerSpecificData: {} } as Record<string, unknown>,
|
||||
true
|
||||
) as Record<string, string>;
|
||||
assert.equal(headers["Authorization"], "Bearer oauth-token");
|
||||
assert.equal(headers["x-api-key"], undefined);
|
||||
});
|
||||
|
||||
// ── existing behavior preserved ─────────────────────────────────────────
|
||||
|
||||
test("claude provider with apiKey on default baseUrl: x-api-key only, respects existing behavior", () => {
|
||||
const executor = new DefaultExecutor("claude");
|
||||
const headers = executor.buildHeaders({ apiKey: "claude-key" } as Record<string, unknown>, true) as Record<string, string>;
|
||||
assert.equal(headers["x-api-key"], "claude-key");
|
||||
assert.equal(headers["Authorization"], undefined);
|
||||
});
|
||||
Reference in New Issue
Block a user