Authorization header lost when falling back from WebSocket to HTTPS with custom openai_base_url

Open 💬 6 comments Opened Mar 23, 2026 by fan454705762-sketch
💡 Likely answer: A maintainer (github-actions[bot], contributor) responded on this thread — see the highlighted reply below.

What version of Codex CLI is running?

0.116.0

What subscription do you have?

When using a custom openai_base_url pointing to an OpenAI-compatible gateway, Codex CLI fails to send the Authorization: Bearer header during the HTTPS fallback after WebSocket connection fails.

Which model were you using?

_No response_

What platform is your computer?

_No response_

What terminal emulator and version are you using (if applicable)?

_No response_

What issue are you seeing?

Bug Description

When using a custom openai_base_url pointing to an OpenAI-compatible gateway, Codex CLI fails to send the Authorization: Bearer header during the HTTPS fallback after WebSocket connection fails.

Environment

  • codex-cli: 0.116.0
  • OS: Linux (Docker container, node:22-bookworm-slim)
  • Gateway: New API (OpenAI-compatible gateway supporting /v1/responses endpoint)

What steps can reproduce the bug?

  1. Set up an OpenAI-compatible gateway that supports the Responses API (/v1/responses) but not WebSocket
  2. Run:
OPENAI_API_KEY="sk-valid-key" codex exec --json --skip-git-repo-check \
  -c 'openai_base_url="https://my-gateway.example.com/v1"' \
  --model gpt-5.4 --dangerously-bypass-approvals-and-sandbox - <<< "say hello"

### What is the expected behavior?

Expected Behavior
After WebSocket fallback, Codex should send Authorization: Bearer <OPENAI_API_KEY> in the HTTPS request to /v1/responses.

Actual Behavior
WebSocket connection to wss://my-gateway.example.com/v1/responses fails (expected, gateway doesn't support WebSocket)
Falls back to HTTPS at https://my-gateway.example.com/v1/responses
Gets 401 Unauthorized because no Authorization header is sent

### Additional information

Verification
The same gateway works perfectly with curl:

curl -s \
  -H "Authorization: Bearer sk-valid-key" \
  -H "Content-Type: application/json" \
  -d '{"model":"gpt-5.4","input":"say hi","stream":true}' \
  https://my-gateway.example.com/v1/responses
# Returns correct SSE stream with response.completed event
Also, the same provider mode has the same issue:

OPENAI_API_KEY="sk-valid-key" codex exec --json --skip-git-repo-check \
  -c 'model_provider="mygateway"' \
  -c 'model_providers.mygateway.base_url="https://my-gateway.example.com/v1"' \
  -c 'model_providers.mygateway.wire_api="responses"' \
  -c 'model_providers.mygateway.requires_openai_auth=true' \
  --model gpt-5.4 --dangerously-bypass-approvals-and-sandbox - <<< "say hello"
# Also gets 401 on HTTPS fallback
This suggests the auth header is being dropped during the WebSocket → HTTPS fallback path when using any custom base URL.

View original on GitHub ↗

6 Comments

github-actions[bot] contributor · 3 months ago

Potential duplicates detected. Please review them and close your issue if it is a duplicate.

  • #15488
  • #13888

Powered by Codex Action

majiayu000 · 3 months ago

Hi, I've been looking into this and traced the root cause.

Proposed approach: Trace the WebSocket→HTTPS fallback code path in codex-cli. When WS connection fails with a custom openai_base_url, the HTTPS fallback constructs a new request but drops the Authorization Bearer header. Fix by ensuring the auth headers (constructed from OPENAI_API_KEY or provider config) are forwarded to the HTTPS request in the fallback path. Add a test verifying auth headers survive the fallback. Comment on issue first per comment-before-pr rule.

I'll open a draft PR shortly. Happy to adjust based on your preference.

pakrym-oai contributor · 3 months ago

Codex doesn't support OPENAI_API_KEY by default. You can use the env var by defining the provider explicitly:

[model_providers.my_cool_provider]
base_url = "http://...." 
env_key = "OPENAI_API_KEY"
fan454705762-sketch · 3 months ago

Thanks @pakrym-oai for the clarification. Defining the provider explicitly with env_key does resolve the auth issue.

However, I'd like to flag a separate efficiency problem in the same scenario: when openai_base_url points to a gateway that doesn't support WebSocket, the WS handshake returns 404 Not Found. Currently in stream_responses_websocket() 1, only 426 UPGRADE_REQUIRED triggers an immediate FallbackToHttp. A 404 falls through to the generic error path, gets classified as retryable, and exhausts the full 5-retry budget before finally falling back to HTTP.

404 is semantically equivalent to 426 here — both are deterministic server responses indicating "this WS endpoint doesn't exist." Retrying won't change the outcome. Network errors (timeouts, connection resets, 5xx) would still be retried normally.

Proposed fix: add || status == StatusCode::NOT_FOUND to the existing 426 match arm. One-line change, zero risk to existing behavior.

Should I open a separate issue for this, or would you prefer I file a PR if the approach looks right?

JaysonGeng · 3 months ago

I picked up the follow-up discussed here: after env_key/explicit-provider config resolves the auth misunderstanding, the remaining real bug is that a deterministic WebSocket handshake 404 Not Found still burns the full retry budget instead of falling back to HTTP immediately.

I pushed a minimal fix here:

What changed:

  • treat websocket handshake 404 Not Found the same way as 426 Upgrade Required in stream_responses_websocket(), i.e. immediate FallbackToHttp
  • add a regression test for 404
  • keep the existing retry-path tests meaningful by explicitly mounting 503 handshake failures there

Local validation:

  • just fix -p codex-core
  • just fmt
  • git diff --check
  • just argument-comment-lint is blocked in this environment because bazel is not installed
  • cargo test -p codex-core websocket_fallback_ passed earlier for the behavioral change; after the final clippy-only pattern-match cleanup, rerunning it in this machine failed during test-binary linking with ld: write() failed, errno=28 because the workspace only had about 517 MiB free

Since upstream PRs are invitation-only here, I am not opening a direct PR. If this direction looks right, a maintainer can invite a PR from the branch above or cherry-pick ace4a7b.

stefanzhangp-debug · 1 day ago

This looks like a real fallback-path issue rather than a normal key/config problem.
From your reproduction, curl works because it sends:
Authorization: Bearer <key>
But Codex seems to drop the Authorization header only after the WebSocket request fails and falls back to HTTPS.
A useful way to isolate it may be:

  1. Log the headers received by the gateway on the initial WebSocket attempt.
  2. Log the headers received by the HTTPS fallback request.
  3. Compare whether the fallback path still applies the same provider auth config.
  4. Test both config styles:
  • openai_base_url
  • model_providers.<name>.base_url + requires_openai_auth=true

If the header is present in the first path but missing in the fallback path, then it is probably not a gateway issue. It is likely that the custom provider auth config is not being reused during fallback.

This is especially important for OpenAI-compatible gateways that support /v1/responses but do not support WebSocket.