TLSv1.3-only streamable HTTP MCP behind nginx fails with native-tls backend on macOS

Resolved 💬 5 comments Opened Nov 19, 2025 by MaximilianAzendorf Closed Apr 18, 2026
💡 Likely answer: A maintainer (etraut-openai, contributor) responded on this thread — see the highlighted reply below.

Summary

When Codex is configured to use a streamable HTTP MCP server behind an nginx 1.18.0 reverse proxy that only offers TLSv1.3, Codex fails to connect on macOS with a low-level TLS error originating from the native TLS stack:

rmcp::transport::worker: worker quit with fatal: Transport channel closed, when Client(reqwest::Error { kind: Request, url: "https://<endpoint>", source: hyper_util::client::legacy::Error(Connect, Error { code: -9836, message: "bad protocol version" }) })

Enabling TLSv1.2 on the nginx ssl_protocols line immediately makes the endpoint work again with Codex, while TLSv1.3 alone still works fine with other clients.

Environment

  • Codex CLI: 0.58.0 (Rust backend)
  • OS: macOS (Apple Silicon)
  • Server: simple HTTPS endpoint behind nginx 1.18.0
  • nginx TLS config (simplified):
  • ssl_protocols TLSv1.3; (failing case)
  • ssl_protocols TLSv1.2 TLSv1.3; (working case)

Minimal reproduction (Codex + minimal nginx endpoint)

This repro uses Codex configured with a streamable HTTP MCP entry, but the MCP URL just points at a trivial nginx location that returns a static 200 response. The backend does not need to implement the MCP protocol; the failure occurs at the TLS layer before HTTP semantics matter.

  1. Configure nginx with a TLSv1.3-only test endpoint. For example:

```nginx
server {
listen 443 ssl http2;
server_name tls13-only.example.com;

ssl_protocols TLSv1.3; # failing case
# ssl_protocols TLSv1.2 TLSv1.3; # working case

# certificate directives elided for brevity

location /test {
return 200 'ok';
}
}
```

Reload nginx so the TLSv1.3-only config is active.

  1. Add a streamable HTTP MCP entry in Codex that points at this endpoint. In ~/.codex/config.toml:

``toml
[mcp_servers.tls13_test]
transport = "streamable_http"
url = "https://tls13-only.example.com/test"
``

  1. Run Codex against this MCP server with a simple input. For example:

``bash
codex \
--config default_mcp_server_name=tls13_test \
exec --skip-git-repo-check "ping"
``

  1. Observe the failure on macOS with TLSv1.3-only nginx. Codex logs an error like:

``text
rmcp::transport::worker: worker quit with fatal: Transport channel closed, when Client(reqwest::Error { kind: Request, url: "https://tls13-only.example.com/test", source: hyper_util::client::legacy::Error(Connect, Error { code: -9836, message: "bad protocol version" }) })
``

  1. Flip nginx back to allow TLSv1.2 and retest. Change ssl_protocols to:

``nginx
ssl_protocols TLSv1.2 TLSv1.3;
``

Reload nginx and re-run the same codex exec command; Codex now successfully connects to the endpoint (the MCP handshake will fail later because /test is not actually an MCP server, but the TLS error disappears).

Root cause analysis

  • codex-rmcp-client depends on:

``toml
reqwest = { version = "0.12", default-features = false, features = ["json", "stream", "rustls-tls"] }
``

which indicates the intention to use the rustls TLS backend for MCP HTTP.

  • Elsewhere in the workspace, reqwest was pulled in with default features (which include native-tls), for example:

```toml
# codex-rs/Cargo.toml
reqwest = "0.12"

# codex-rs/lmstudio/Cargo.toml (upstream main)
reqwest = { version = "0.12", features = ["json", "stream"] }

# codex-rs/tui/Cargo.toml (upstream main)
reqwest = { version = "0.12", features = ["json"] }
```

  • Cargo unified these features, so the final reqwest 0.12 in the Codex CLI had both native-tls and rustls-tls enabled.
  • In that combined configuration, reqwest on macOS chooses the OS-native TLS backend by default. Against a TLSv1.3-only nginx 1.18.0 endpoint, that backend fails with the OSStatus -9836 ("bad protocol version") error, which Codex sees.
  • A standalone reproducer using reqwest 0.12 + native-tls (no Codex code) fails in the same way against the same endpoint, while a reqwest 0.12 + rustls-tls client succeeds, confirming that the failure is tied to the native TLS backend rather than Codex logic or rustls.

Proposed fix

Use reqwest with default-features = false at the workspace level and explicitly turn on rustls-tls in the crates that need HTTPS, so that Codex’s RMCP HTTP client uses rustls consistently:

  • In codex-rs/Cargo.toml:

```toml
[workspace.dependencies]

  • reqwest = "0.12"
  • reqwest = { version = "0.12", default-features = false }

```

  • In crates that use reqwest directly (e.g. codex-rs/lmstudio, codex-rs/tui), change:

``toml
reqwest = { version = "0.12", features = ["json", ...] }
``

to:

``toml
reqwest = { version = "0.12", default-features = false, features = ["json", ..., "rustls-tls"] }
``

A draft PR implementing this approach is available here: https://github.com/openai/codex/pull/6873

Impact

  • This resolves the TLS handshake failure for TLSv1.3-only HTTP endpoints behind nginx on macOS when accessed via Codex’s RMCP HTTP client.
  • It also aligns the implementation with the apparent intent in codex-rmcp-client to use rustls for MCP HTTP traffic.

View original on GitHub ↗

5 Comments

zolgear · 7 months ago

The same issue occurred when connecting to LiteLLM behind nginx.

Logs output by the VS Code extension:

mcp startup: no servers
2026-01-19T01:31:10.158001Z ERROR codex_api::endpoint::responses: error=network error: error sending request for url (https://litellm.domain.com/responses)
Reconnecting... 1/5 (stream disconnected before completion: error sending request for url (https://litellm.domain.com/responses))
2026-01-19T01:31:13.285313Z ERROR codex_api::endpoint::responses: error=network error: error sending request for url (https://litellm.domain.com/responses)
etraut-openai contributor · 4 months ago

Closing outdated bug.

MichaelErmer · 4 months ago
The same issue occurred when connecting to LiteLLM behind nginx. Logs output by the VS Code extension: `` mcp startup: no servers 2026-01-19T01:31:10.158001Z ERROR codex_api::endpoint::responses: error=network error: error sending request for url (https://litellm.domain.com/responses) Reconnecting... 1/5 (stream disconnected before completion: error sending request for url (https://litellm.domain.com/responses)) 2026-01-19T01:31:13.285313Z ERROR codex_api::endpoint::responses: error=network error: error sending request for url (https://litellm.domain.com/responses) ``

FYI: you need to change your LiteLLM proxy to allow TLS1.2, codex can not do TLS1.3.

cjangrist · 4 days ago

I can confirm this bug is still very much alive on the latest release, and — importantly — it's not MCP-specific. It also breaks the core model-provider /v1/responses streaming connection, which matches what @zolgear reported above against codex_api::endpoint::responses. Given that, I think closing this as "outdated" was premature; requesting a reopen.

Environment

  • OS: macOS 27.0 (BuildVersion 26A5416b), Darwin Kernel 27.0.0, arm64 (Apple Silicon)
  • Codex CLI: reproduced on both 0.147.0 and 0.149.0 (latest at time of testing) — updating did not fix it
  • Install method: npm global (npm install -g @openai/codex)
  • Node: v26.7.0 / npm 11.19.0
  • Reproduces in both the interactive TUI and headless codex exec
  • model_provider = "proxy" custom OpenAI-compatible provider, wire_api = "responses", base_url pointed at a Cloudflare-fronted origin that enforces a TLS 1.3 minimum
  • No system proxy, no VPN exit node active, no MDM/endpoint security software (codex doctor confirms all of this — see below)
  • Identical config.toml and identical Codex version work reliably against the same endpoint from Linux hosts (where native-tls maps to OpenSSL) — only macOS reproduces it, consistent with this issue's title/diagnosis of the native-tls/Secure Transport backend.

codex doctor self-diagnosis

✗ reachability one or more required provider endpoints are unreachable over HTTP — Check proxy, VPN, firewall, DNS, and custom CA configuration.
    reachability mode        provider auth
    proxy API inference URL  https://<redacted>/v1/<redacted> TLS handshake or certificate validation failed (required)
✓ network      no proxy env vars: none · system proxy: direct · respect system proxy: disabled

Packet-level root cause (this is the part I think is new information)

I captured traffic during live failures. The pattern is deterministic and clarifies what's actually happening — and it contradicts the workaround suggested above ("codex can not do TLS1.3"). Codex can negotiate TLS 1.3 correctly — it does so successfully on some connection attempts. The bug is that it's inconsistent: it silently alternates between two different TLS behaviors across connection attempts within the same run, and roughly 80-90% of new connections land on the broken one.

Failing attempts (the majority, in bursts of many rapid reconnects):

  1. TCP handshake completes normally (SYN/SYN-ACK/ACK).
  2. Client sends a 181-byte ClientHello.
  3. Server immediately replies with a 7-byte TLS record + FIN: bytes 15 03 01 00 02 02 46 — TLS record type 0x15 (Alert), level 0x02 (fatal), description 0x46 = 70 = protocol_version.
  4. Decoding the ClientHello: client_version = 0x0303 (TLS 1.2), cipher suite list is all legacy CBC/GCM suites (no 0x1301/0x1302/0x1303), supported_groups only lists secp256r1/secp384r1/secp521r1 (no x25519), 8-entry legacy signature_algorithms, and no supported_versions (0x002b) or key_share (0x0033) extension at all — i.e. it never advertises TLS 1.3 support. No GREASE values either. This fingerprint doesn't match rustls or OpenSSL/BoringSSL output — it matches Apple Secure Transport's classic ClientHello shape.
  5. The destination enforces a TLS 1.3 minimum, so it correctly, fatally rejects this handshake. This is what surfaces to the user as stream disconnected before completion: error sending request for url (...).

Succeeding attempts (the minority): full modern TLS 1.3 ClientHello with supported_versions, key_share (observed with a post-quantum hybrid group, X25519MLKEM768), GREASE, and modern cipher suites — negotiates cleanly, streams the SSE response to completion with no errors.

In one captured burst I counted 9 failed connection attempts out of 10 before one landed on the working path. curl against the exact same URL, same machine, same moment, succeeds via modern TLS 1.3 on 100% of attempts (obviously since it doesn't have this dual-backend issue) — confirming this isn't a server-side/network issue, DNS, VPN, or MTU problem, it's purely which TLS backend/code path a given Codex connection attempt happens to use.

Why this matters beyond MCP

This confirms the reqwest/native-tls vs rustls-tls split described in the original report isn't isolated to codex-rmcp-client — the core codex_http_client used for the primary Responses API streaming connection exhibits the exact same intermittent backend selection, with the exact same protocol_version fatal alert. Anyone whose OpenAI-compatible endpoint (self-hosted, LiteLLM, Cloudflare-fronted, etc.) enforces TLS 1.3 will hit this on macOS specifically, on the main request path, not just MCP.

Happy to provide the full raw RUST_LOG=trace logs / pcap if useful for triage — trimmed here for length and to avoid pasting any account-identifying info.

etraut-openai contributor · 4 days ago

@cjangrist, this bug report is very stale. If you are still seeing similar symptoms with the latest versions, please open a new issue and include the details requested in the bug report template.