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

  1. If you embed a custom dialer/transport, ensure every addr passed through is host:port (default :443 for https).
  2. Log the offending addr value at debug level to find which layer produced it.
  3. 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

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

Related errors


AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15). Data as JSON: /api/errors/6138b85a942658db. Report an issue: GitHub.