router-for-me/CLIProxyAPI · critical

claude oauth tls: apply ClientHello: %w

Error message

claude oauth tls: apply ClientHello: %w

What it means

tls.UClient was constructed but ApplyPreset(claudeOAuthTLSClientHelloSpec()) rejected the ClientHello specification used to mimic Claude Code's TLS fingerprint. The spec is a fixed in-code structure; a failure means the compiled uTLS library considers it invalid — effectively a build/version mismatch, not a runtime condition.

Source

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

		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)
		}
		return nil, fmt.Errorf("claude oauth tls: handshake upstream: %w", errHandshake)
	}
	return httpwire.NewOrderedRequestConn(tlsConn, claudeOAuthRequestHeaderOrder), nil
}

func (t *utlsRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
	return t.transport.RoundTrip(req)

View on GitHub (pinned to 78f0c4079e)

Solutions

  1. Restore the utls version pinned by this repo's go.mod (go mod tidy with the committed go.sum).
  2. If you maintain a fork, re-validate your ClientHelloSpec against the utls version you build with (fields like CipherSuites/Extensions changed across releases).
  3. File an issue with the repo including your utls version if the stock build fails here.
Defensive patterns

Strategy: fallback

Validate before calling

// at build time: ensure the utls version matches the pinned one
// go mod verify && go mod tidy

Try / catch

if errPreset := tlsConn.ApplyPreset(spec); errPreset != nil {
    // binary was built against an incompatible utls: rebuild with the repo's go.mod pins
    return fmt.Errorf("rebuild with pinned dependencies: %w", errPreset)
}

Prevention

When it happens

Trigger: Building the binary against an incompatible github.com/refraction-networking/utls version whose ClientHelloSpec fields/expectations changed; hand-edited fingerprint specs in forks; spec structs zeroed by unusual build flags. Never depends on the remote server or network.

Common situations: go.mod upgraded (or downgraded) utls transitively so the vendored spec no longer validates; forks that tweak claudeOAuthTLSClientHelloSpec; mixed module cache states after partial upgrades.

Understand the failure class

Related errors


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