router-for-me/CLIProxyAPI · error

claude oauth tls: dial upstream

Error message

claude oauth tls: dial upstream: %w

What it means

The uTLS-based OAuth transport failed at the raw TCP/proxy dial stage, before any TLS bytes flowed. dialTLSContext first connects through the configured (possibly proxy-aware) dialer; a failure here means network-level connectivity to the OAuth host (or to the configured proxy) is broken. The wrap includes the dialer's own error (DNS, refused, timeout, proxy failure).

Solutions

  1. Check the wrapped error: DNS failure → fix resolv.conf/DNS; connection refused/timeout → verify egress path to the OAuth host.
  2. Set or fix HTTPS_PROXY/HTTP_PROXY/NO_PROXY so the OAuth dialer uses the corporate proxy (and ensure the proxy allows CONNECT to the OAuth hosts).
  3. Test raw reachability: `curl -v https://api.anthropic.com` (with and without -x $HTTPS_PROXY) from the same host/container.
  4. In containers, verify DNS and egress with a simple Go or curl probe before running the login/refresh flow.

Example fix

# before
unset HTTPS_PROXY   # direct egress blocked by firewall

# after
export HTTPS_PROXY=http://proxy.corp.example:3128
export NO_PROXY=localhost,127.0.0.1
Defensive patterns

Strategy: retry

Validate before calling

conn, err := net.DialTimeout("tcp", "api.anthropic.com:443", 5*time.Second)
if err != nil { return fmt.Errorf("egress to OAuth host blocked: %w", err) }
conn.Close()

Try / catch

resp, err := oauthClient.Do(req)
if err != nil && strings.Contains(err.Error(), "dial upstream") {
    if isTransientNetErr(err) { time.Sleep(time.Second); resp, err = oauthClient.Do(req) }
    if err != nil { return fmt.Errorf("check DNS/proxy egress: %w", err) }
}

Prevention

When it happens

Trigger: The Claude OAuth HTTP client dialing api.anthropic.com/claude.ai through direct or proxy dialing when: DNS resolution fails, the network is down, a firewall drops egress, or HTTPS_PROXY/ALL_PROXY points at a dead/unreachable proxy.

Common situations: Corporate environments where egress requires a proxy but HTTPS_PROXY is unset (direct connections blocked); misconfigured proxy env vars (typo'd host, wrong port, proxy requiring auth); DNS failures in containers with broken resolv.conf; firewalls blocking the OAuth endpoints specifically.

Understand the failure class

Related errors


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

Appendix: source

Thrown at internal/auth/claude/utls_transport.go:212

	roundTripper.transport = &http.Transport{
		ForceAttemptHTTP2: false,
		DialTLSContext:    roundTripper.dialTLSContext,
	}
	return roundTripper
}

func (t *utlsRoundTripper) dialTLSContext(ctx context.Context, network, addr string) (net.Conn, error) {
	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 {

View on GitHub (pinned to 78f0c4079e)