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

Resolved 💬 3 comments Opened Nov 19, 2025 by MaximilianAzendorf Closed Apr 18, 2026

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 ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗