{"record":{"id":"e457d199b70fdb04","repo":"caddyserver/caddy","slug":"http-transport-tls-handshake-ds-timeout","errorCode":null,"errorMessage":"HTTP transport TLS handshake %ds timeout","messagePattern":"HTTP transport TLS handshake (.+?)s timeout","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"modules/caddyhttp/reverseproxy/httptransport.go","lineNumber":453,"sourceCode":"\t\t\t\ttlsConfig := rt.TLSClientConfig.Clone()\n\t\t\t\tif serverNameHasPlaceholder {\n\t\t\t\t\trepl := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer)\n\t\t\t\t\ttlsConfig.ServerName = repl.ReplaceAll(tlsConfig.ServerName, \"\")\n\t\t\t\t}\n\n\t\t\t\t// h1 only\n\t\t\t\tif caddyhttp.GetVar(ctx, tlsH1OnlyVarKey) == true {\n\t\t\t\t\t// stdlib does this\n\t\t\t\t\t// https://github.com/golang/go/blob/4837fbe4145cd47b43eed66fee9eed9c2b988316/src/net/http/transport.go#L1701\n\t\t\t\t\ttlsConfig.NextProtos = nil\n\t\t\t\t}\n\n\t\t\t\ttlsConn := tls.Client(conn, tlsConfig)\n\n\t\t\t\t// complete the handshake before returning the connection\n\t\t\t\tif rt.TLSHandshakeTimeout != 0 {\n\t\t\t\t\tvar cancel context.CancelFunc\n\t\t\t\t\tctx, cancel = context.WithTimeoutCause(ctx, rt.TLSHandshakeTimeout, fmt.Errorf(\"HTTP transport TLS handshake %ds timeout\", int(rt.TLSHandshakeTimeout.Seconds())))\n\t\t\t\t\tdefer cancel()\n\t\t\t\t}\n\t\t\t\terr = tlsConn.HandshakeContext(ctx)\n\t\t\t\tif err != nil {\n\t\t\t\t\t_ = tlsConn.Close()\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\treturn tlsConn, nil\n\t\t\t}\n\t\t}\n\t}\n\n\tif h.KeepAlive != nil {\n\t\t// according to https://pkg.go.dev/net#Dialer.KeepAliveConfig,\n\t\t// KeepAlive is ignored if KeepAliveConfig.Enable is true.\n\t\t// If configured to 0, a system-dependent default is used.\n\t\t// To disable tcp keepalive, choose a negative value,\n\t\t// so KeepAliveConfig.Enable is false and KeepAlive is negative.","sourceCodeStart":435,"sourceCodeEnd":471,"githubUrl":"https://github.com/caddyserver/caddy/blob/50e54ee279aa1e504fe218ca49ab6ae16c100410/modules/caddyhttp/reverseproxy/httptransport.go#L435-L471","documentation":"Not thrown by itself: it is a timeout *cause* attached via `context.WithTimeoutCause` around `tlsConn.HandshakeContext` in the custom DialTLSContext path (used when ServerName has placeholders or proxy_protocol is on). If the upstream TLS handshake exceeds tls_handshake_timeout, the returned error unwraps to this cause string (`%ds` is the configured seconds).","triggerScenarios":"Upstream TLS server unreachable/hanging (firewall drops SYN-ACK, blackholed network), TLS handshake storms with a slow upstream, or tls_handshake_timeout set too low for high-latency links. Happens only when DialTLSContext customization is active (placeholder in tls_server_name or proxy_protocol enabled).","commonSituations":"Cloud firewall/security-group blocking the proxy's egress on 443; upstream behind a slow link; mTLS where the server requests and waits on client certs; timeouts triggered during upstream restarts.","solutions":["Verify egress connectivity to the upstream: curl -v https://upstream:port from the Caddy host","Increase tls_handshake_timeout in the transport if the network is legitimately high-latency","Check upstream TLS health (certificate expiry, overload) — handshakes that stall often mean a dead backend","Ensure the security group/firewall allows outbound TCP to the upstream port"],"exampleFix":"# before\ntransport http {\n\ttls_handshake_timeout 2s\n}\n# after\ntransport http {\n\ttls_handshake_timeout 10s\n}","handlingStrategy":"retry","validationCode":"// preflight upstream reachability before config rollout\nconn, err := net.DialTimeout(\"tcp\", upstream, 5*time.Second)\nif err != nil {\n\tlog.Printf(\"upstream unreachable: %v\", err)\n}\nconn.Close()","typeGuard":null,"tryCatchPattern":"// errors.Is via context cause\nerr := respErr\nvar cause interface{ Timeout() bool }\nif errors.As(err, &cause) || strings.Contains(err.Error(), \"TLS handshake\") {\n\t// treat as transient: retry next upstream / mark unhealthy\n\tproxy.MarkUnhealthy(upstream)\n}","preventionTips":["Open egress firewall rules to upstream ports before enabling proxy_protocol/placeholder SNI configs","Size tls_handshake_timeout to your worst-case RTT plus cert-chain cost","Monitor handshake-failure rates per upstream to catch dead backends early"],"tags":["reverse-proxy","tls","timeout","network"],"backgroundTag":null,"analyzedSha":"50e54ee279aa1e504fe218ca49ab6ae16c100410","analyzedAt":"2026-08-15T09:20:21.641Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}