{"record":{"id":"3e144a3b7c8de91b","repo":"cloudflare/cloudflared","slug":"flow-was-closed-directly","errorCode":null,"errorMessage":"flow was closed directly","messagePattern":"flow was closed directly","errorType":"exception","errorClass":"SessionCloseErr","httpStatus":null,"severity":"info","filePath":"quic/v3/session.go","lineNumber":36,"sourceCode":"\t// A default is provided in the case that the client does not provide a close idle timeout.\n\tdefaultCloseIdleAfter = 210 * time.Second\n\n\t// The maximum payload from the origin that we will be able to read. However, even though we will\n\t// read 1500 bytes from the origin, we limit the amount of bytes to be proxied to less than\n\t// this value (maxDatagramPayloadLen).\n\tmaxOriginUDPPacketSize = 1500\n\n\t// The maximum amount of datagrams a session will queue up before it begins dropping datagrams.\n\t// This channel buffer is small because we assume that the dedicated writer to the origin is typically\n\t// fast enought to keep the channel empty.\n\twriteChanCapacity = 512\n\n\tlogFlowID        = \"flowID\"\n\tlogPacketSizeKey = \"packetSize\"\n)\n\n// SessionCloseErr indicates that the session's Close method was called.\nvar SessionCloseErr error = errors.New(\"flow was closed directly\") //nolint:errname\n\n// SessionIdleErr is returned when the session was closed because there was no communication\n// in either direction over the session for the timeout period.\ntype SessionIdleErr struct { //nolint:errname\n\ttimeout time.Duration\n}\n\nfunc (e SessionIdleErr) Error() string {\n\treturn fmt.Sprintf(\"flow was idle for %v\", e.timeout)\n}\n\nfunc (e SessionIdleErr) Is(target error) bool {\n\t_, ok := target.(SessionIdleErr)\n\treturn ok\n}\n\nfunc newSessionIdleErr(timeout time.Duration) error {\n\treturn SessionIdleErr{timeout}","sourceCodeStart":18,"sourceCodeEnd":54,"githubUrl":"https://github.com/cloudflare/cloudflared/blob/2253eeeb25a44a713a4b60b8ba1e1b3f377d1a0f/quic/v3/session.go#L18-L54","documentation":"SessionCloseErr is the sentinel error returned by a v3 QUIC session's Serve when the session's Close method was called directly (as opposed to timing out). It signals an intentional, successful shutdown, so the muxer treats it as a normal end-of-session condition and logs at debug level rather than reporting a failure.","triggerScenarios":"Calling session.Close() while the session's Serve loop is running — e.g. on proxy shutdown, connection index reuse, or flow teardown from the muxer; also produced by mockSession in tests to simulate clean shutdown.","commonSituations":"Graceful cloudflared shutdown or config reload closing active tunnels; a session being closed by another goroutine while Serve is still waiting on streams; developers mistaking this benign sentinel for a transport failure in logs.","solutions":["No fix needed — treat it as success: check errors.Is(err, v3.SessionCloseErr) and return without logging an error.","If you see it unexpectedly, audit which component calls Close (shutdown hooks, conn index management) and confirm it was intentional.","When wrapping Serve's error, propagate it with %w so errors.Is-based checks in the muxer still match.","In tests, return SessionCloseErr to simulate a normally-closed session instead of a generic error."],"exampleFix":"// before\nif err := session.Serve(ctx); err != nil {\n    log.Error().Err(err).Msg(\"session failed\")\n}\n// after\nif err := session.Serve(ctx); err != nil && !errors.Is(err, v3.SessionCloseErr) && !errors.Is(err, v3.SessionIdleErr{}) {\n    log.Error().Err(err).Msg(\"session failed\")\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"func isCleanSessionClose(err error) bool {\n    return errors.Is(err, v3.SessionCloseErr) || errors.Is(err, v3.SessionIdleErr{})\n}","tryCatchPattern":"if err := session.Serve(ctx); err != nil {\n    if errors.Is(err, v3.SessionCloseErr) || errors.Is(err, v3.SessionIdleErr{}) {\n        log.Debug().Msgf(\"flow closed: %s\", err.Error())\n        return nil\n    }\n    return err\n}","preventionTips":["Always classify Serve errors with errors.Is against SessionCloseErr/SessionIdleErr before logging as failures.","Wrap returned errors with %w so sentinel checks keep working.","Document intentional Close calls so unexpected sentinels stand out."],"tags":["quic","session","shutdown","sentinel-error"],"backgroundTag":"invalid-state-transition","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"}