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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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