Skip to content

[Bug]: CORS preflight rejects all browser SDK clients — Access-Control-Allow-Headers omits X-Stainless-* #1773

Description

@GLY2024

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

  1. ocx start --port 10100
  2. 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"}]}',
});
  1. Preflight is rejected; console shows Request header field x-stainless-os is not allowed by Access-Control-Allow-Headers in preflight response.
  2. 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

  • I searched existing issues and documentation.
  • I removed secrets, tokens, account details, request credentials, and personal data.

Metadata

Metadata

Assignees

No one assigned

    Labels

    account-poolOAuth, credentials, Codex pool, quota, failover, plansbugSomething isn't workingproxyHTTP proxy, routing, reverse-proxy / management auth

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions