Client or integration
Direct HTTP/API client
Area
Proxy and routing
Summary
corsHeaders() in src/server/auth-cors.ts returns a hard-coded Access-Control-Allow-Headers list that omits the X-Stainless-* header family. Stainless-generated SDKs (Anthropic TS SDK, OpenAI TS SDK) attach X-Stainless-OS, -Lang, -Arch, -Runtime, -Runtime-Version, -Package-Version, -Retry-Count, -Timeout to every request, so any browser client built on one fails CORS preflight and can never reach the proxy — no matter how it is configured.
Expected: browser-based SDK clients can call the proxy.
The proxy already emits this same header family upstream itself (src/adapters/client-fingerprint.ts); the names just never made it into the CORS list.
This looks like a recurring pattern rather than a one-off omission: #379 appended ChatGPT-Account-Id to this same literal for ChatGPT/Codex App voice preflights, and OpenAI-Alpha … X-OAI-Attestation were appended after that for GPT-Live. Every new browser client costs another manual entry, and the failure mode each time is a request that never leaves the browser.
Suggested fix — echo the preflight's requested headers, keeping the static list as fallback:
const requestedHeaders = req?.headers.get("Access-Control-Request-Headers");
// ...
"Access-Control-Allow-Headers": requestedHeaders ?? "Content-Type, Authorization, /* …existing… */",
"Vary": "Origin, Access-Control-Request-Headers",
This is the default behaviour of the standard cors middleware. Access control is unaffected — Access-Control-Allow-Origin and the existing isAllowedRequestOrigin() check remain the gate; the header list cannot grant an origin that was already rejected. Verified: applied locally against 2.14.0, the affected client now connects and streams normally.
Reproduction
ocx start --port 10100
- Open devtools on any HTTPS page, run:
await fetch('http://127.0.0.1:10100/v1/messages', {
method: 'POST',
headers: {
'content-type': 'application/json',
'anthropic-version': '2023-06-01',
'x-api-key': '<key>',
'x-stainless-os': 'Windows', // every Stainless SDK sends this automatically
},
body: '{"model":"<any>","max_tokens":16,"messages":[{"role":"user","content":"hi"}]}',
});
- Preflight is rejected; console shows
Request header field x-stainless-os is not allowed by Access-Control-Allow-Headers in preflight response.
- Drop
x-stainless-os from the headers and the same request succeeds.
Worth noting for triage: the preflight returns 204 and the identical request from curl returns 200, because only a browser compares the returned allow-list against the headers it intends to send. Every non-browser probe reports "gateway healthy", which makes this look like a connectivity problem rather than a CORS one.
Real-world trigger: the Claude for Office Word add-in (origin https://pivot.claude.ai, Anthropic TS SDK) — it surfaces the failure only as Connection error.
Version
2.14.0
Operating system
Windows 11
Provider and model
Not provider-specific — reproduced with deepseek/deepseek-v4-flash, but the request never leaves the browser, so any model behaves the same.
Logs or error output
# Browser console (Claude for Office add-in)
Access to fetch at 'http://127.0.0.1:10100/v1/messages' from origin 'https://pivot.claude.ai'
has been blocked by CORS policy: Request header field x-stainless-os is not allowed by
Access-Control-Allow-Headers in preflight response.
POST http://127.0.0.1:10100/v1/messages net::ERR_FAILED
Uncaught (in promise) Connection error.
Caused by: TypeError: Failed to fetch (127.0.0.1:10100)
# Preflight succeeds at the HTTP level, which is why this is easy to misdiagnose
$ curl -s -o /dev/null -w "%{http_code}\n" -X OPTIONS http://127.0.0.1:10100/v1/messages \
-H "Origin: https://pivot.claude.ai" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: content-type,x-api-key,x-stainless-os"
204
# Allow-list actually returned — no X-Stainless-* entry
Access-Control-Allow-Headers: Content-Type, Authorization, X-OpenCodex-API-Key, X-Api-Key,
Anthropic-Version, Anthropic-Beta, ChatGPT-Account-Id, OpenAI-Alpha, X-Session-Id,
Session-Id, Thread-Id, Originator, X-OAI-Attestation
Checks
Client or integration
Direct HTTP/API client
Area
Proxy and routing
Summary
corsHeaders()insrc/server/auth-cors.tsreturns a hard-codedAccess-Control-Allow-Headerslist that omits theX-Stainless-*header family. Stainless-generated SDKs (Anthropic TS SDK, OpenAI TS SDK) attachX-Stainless-OS,-Lang,-Arch,-Runtime,-Runtime-Version,-Package-Version,-Retry-Count,-Timeoutto every request, so any browser client built on one fails CORS preflight and can never reach the proxy — no matter how it is configured.Expected: browser-based SDK clients can call the proxy.
The proxy already emits this same header family upstream itself (
src/adapters/client-fingerprint.ts); the names just never made it into the CORS list.This looks like a recurring pattern rather than a one-off omission: #379 appended
ChatGPT-Account-Idto this same literal for ChatGPT/Codex App voice preflights, andOpenAI-Alpha … X-OAI-Attestationwere appended after that for GPT-Live. Every new browser client costs another manual entry, and the failure mode each time is a request that never leaves the browser.Suggested fix — echo the preflight's requested headers, keeping the static list as fallback:
This is the default behaviour of the standard
corsmiddleware. Access control is unaffected —Access-Control-Allow-Originand the existingisAllowedRequestOrigin()check remain the gate; the header list cannot grant an origin that was already rejected. Verified: applied locally against 2.14.0, the affected client now connects and streams normally.Reproduction
ocx start --port 10100Request header field x-stainless-os is not allowed by Access-Control-Allow-Headers in preflight response.x-stainless-osfrom the headers and the same request succeeds.Worth noting for triage: the preflight returns
204and the identical request fromcurlreturns200, because only a browser compares the returned allow-list against the headers it intends to send. Every non-browser probe reports "gateway healthy", which makes this look like a connectivity problem rather than a CORS one.Real-world trigger: the Claude for Office Word add-in (origin
https://pivot.claude.ai, Anthropic TS SDK) — it surfaces the failure only asConnection error.Version
2.14.0
Operating system
Windows 11
Provider and model
Not provider-specific — reproduced with
deepseek/deepseek-v4-flash, but the request never leaves the browser, so any model behaves the same.Logs or error output
Checks