{"record":{"id":"9a0eff51fcdeb6a5","repo":"cloudflare/cloudflared","slug":"servetunnel-v","errorCode":null,"errorMessage":"ServeTunnel: %v","messagePattern":"ServeTunnel: (.+?)","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"supervisor/tunnel.go","lineNumber":384,"sourceCode":"\n// ServeTunnel runs a single tunnel connection, returns nil on graceful shutdown,\n// on error returns a flag indicating if error can be retried\nfunc (e *EdgeTunnelServer) serveTunnel(\n\tctx context.Context,\n\tconnLog *ConnAwareLogger,\n\taddr *allregions.EdgeAddr,\n\tconnIndex uint8,\n\tfuse *booleanFuse,\n\tbackoff *protocolFallback,\n\tprotocol connection.Protocol,\n) (err error, recoverable bool) {\n\t// Treat panics as recoverable errors\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tvar ok bool\n\t\t\terr, ok = r.(error)\n\t\t\tif !ok {\n\t\t\t\terr = fmt.Errorf(\"ServeTunnel: %v\", r)\n\t\t\t}\n\t\t\terr = errors.Wrapf(err, \"stack trace: %s\", string(debug.Stack()))\n\t\t\trecoverable = true\n\t\t}\n\t}()\n\n\tdefer e.config.Observer.SendDisconnect(connIndex)\n\terr, recoverable = e.serveConnection(\n\t\tctx,\n\t\tconnLog,\n\t\taddr,\n\t\tconnIndex,\n\t\tfuse,\n\t\tbackoff,\n\t\tprotocol,\n\t)\n\n\tif err != nil {","sourceCodeStart":366,"sourceCodeEnd":402,"githubUrl":"https://github.com/cloudflare/cloudflared/blob/2253eeeb25a44a713a4b60b8ba1e1b3f377d1a0f/supervisor/tunnel.go#L366-L402","documentation":"In ServeTunnel's deferred recover handler, a panic value that is not an `error` cannot be wrapped directly, so it is converted with fmt.Errorf(\"ServeTunnel: %v\", r). This preserves the panic payload (string, runtime error, etc.) as an error, then attaches the goroutine stack trace and marks the tunnel failure as recoverable so the supervisor can retry.","triggerScenarios":"Any goroutine panic inside ServeTunnel's call tree that panics with a non-error value: e.g. panic(\"string literal\"), nil map writes (runtime errors are error, but panic(1) or panic(fmt.Sprintf(...)) are not), or library code panicking with arbitrary values.","commonSituations":"Concurrency bugs (concurrent map read/write panics are runtime errors but can arrive as non-error values depending on throw path), indexing bugs, misconfigured handlers panicking with strings, third-party dependency panics during tunnel serving.","solutions":["Read the attached 'stack trace: ...' wrapper in the error to locate the panicking frame.","Fix the underlying panic cause (nil dereference, bad index, concurrent map access) identified by the stack.","Avoid panic(non-error) in your own code; panic with error values so the recover path preserves types.","Run with `GOTRACEBACK=all` / race detector in testing to surface the concurrency or memory bug causing the panic.","Update cloudflared if the stack points into a dependency; the panic may already be fixed upstream."],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"err := stream.PipeBidirectional(ctx, tunnelConn, originConn)\nif err != nil && strings.Contains(err.Error(), \"timeout waiting for second stream\") {\n\tlog.Warn().Err(err).Msg(\"second stream never arrived; connection terminated\")\n\t// classify as benign for single-stream protocols\n}","preventionTips":["Keep cloudflared and edge protocol versions aligned (upgrade regularly).","Enable TCP keepalives to reap half-open single-stream connections.","Tune maxWaitForSecondStream if the proxied application legitimately opens the second stream late."],"tags":["panic","recovery","goroutine"],"backgroundTag":"internal-invariant-violation","analyzedSha":"2253eeeb25a44a713a4b60b8ba1e1b3f377d1a0f","analyzedAt":"2026-09-06T04:14:33.757Z","contentChangedAt":"2026-09-06T04:14:33.757Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}