router-for-me/CLIProxyAPI · error
claude oauth tls: split upstream address: %w
Error message
claude oauth tls: split upstream address: %w
What it means
After a successful dial, net.SplitHostPort could not split the remote address into host:port. This only happens when the dialer's address lacks a port or is malformed — the transport derives addr from the HTTP client, so in normal operation it is always host:port. Seeing it indicates a custom dialer, address-rewriting proxy layer, or an internal invariant break rather than an environmental problem.
Source
Thrown at internal/auth/claude/utls_transport.go:220
var (
conn net.Conn
err error
)
if contextDialer, ok := t.dialer.(proxy.ContextDialer); ok {
conn, err = contextDialer.DialContext(ctx, network, addr)
} else {
conn, err = t.dialer.Dial(network, addr)
}
if err != nil {
return nil, fmt.Errorf("claude oauth tls: dial upstream: %w", err)
}
host, _, errSplit := net.SplitHostPort(addr)
if errSplit != nil {
if errClose := conn.Close(); errClose != nil {
log.Debugf("claude oauth tls: close failed connection: %v", errClose)
}
return nil, fmt.Errorf("claude oauth tls: split upstream address: %w", errSplit)
}
tlsConn := tls.UClient(conn, newClaudeOAuthTLSConfig(host, t.sessionCache), tls.HelloCustom)
if errPreset := tlsConn.ApplyPreset(claudeOAuthTLSClientHelloSpec()); errPreset != nil {
if errClose := tlsConn.Close(); errClose != nil {
log.Debugf("claude oauth tls: close connection after preset failure: %v", errClose)
}
return nil, fmt.Errorf("claude oauth tls: apply ClientHello: %w", errPreset)
}
handshakeCtx := ctx
if handshakeTimeout, _ := ctx.Value(claudeRefreshHandshakeTimeoutContextKey{}).(time.Duration); handshakeTimeout > 0 {
var cancelHandshake context.CancelFunc
handshakeCtx, cancelHandshake = context.WithTimeout(ctx, handshakeTimeout)
defer cancelHandshake()
}
if errHandshake := tlsConn.HandshakeContext(handshakeCtx); errHandshake != nil {
if errClose := tlsConn.Close(); errClose != nil {
log.Debugf("claude oauth tls: close connection after handshake failure: %v", errClose)
}View on GitHub (pinned to 78f0c4079e)
Solutions
- If you embed a custom dialer/transport, ensure every addr passed through is host:port (default :443 for https).
- Log the offending addr value at debug level to find which layer produced it.
- Revert any local transport wrapping and use the provided transport constructor as-is.
Example fix
// before (custom wrapper drops the port) conn, err := dialer.DialContext(ctx, "tcp", host) // after conn, err := dialer.DialContext(ctx, "tcp", net.JoinHostPort(host, "443"))
Defensive patterns
Strategy: validation
Validate before calling
if _, _, err := net.SplitHostPort(addr); err != nil {
addr = net.JoinHostPort(addr, "443") // normalize before dialing
} Type guard
func isHostPort(addr string) bool { _, _, err := net.SplitHostPort(addr); return err == nil } Prevention
- In custom dialers/transport wrappers, always pass net.JoinHostPort(host, port) shaped addresses.
- Log addr at debug level in wrapped transports to catch malformed addresses early.
When it happens
Trigger: Embedding the utlsRoundTripper with a custom dialer or transport wrapper that passes addrs like "api.anthropic.com" (no port) or a malformed string into dialTLSContext; unit tests stubbing the dialer with synthetic addresses.
Common situations: Forks/SDK embedders composing their own http.Transport with a DialTLSContext that forwards non-standard addr strings; almost impossible via public config in the stock server since the stdlib always supplies host:443.
Understand the failure class
- SSL/TLS and certificate errors — how TLS handshakes and certificate validation fail.
Related errors
- fetch Claude OAuth %s: %w
- claude oauth tls: dial upstream: %w
- claude oauth tls: apply ClientHello: %w
- claude oauth tls: handshake upstream: %w
- antigravity token exchange: execute request: %w
AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15).
Data as JSON: /api/errors/6138b85a942658db.
Report an issue: GitHub.