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
- Restore the utls version pinned by this repo's go.mod (go mod tidy with the committed go.sum).
- If you maintain a fork, re-validate your ClientHelloSpec against the utls version you build with (fields like CipherSuites/Extensions changed across releases).
- 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
- Never bump github.com/refraction-networking/utls independently of this repo's go.mod.
- After any dependency change, run the OAuth transport tests before shipping.
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
- 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: split upstream address: %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/b2e87aed5c639a09.
Report an issue: GitHub.