cloudflare/cloudflared · info · SessionCloseErr
flow was closed directly
Error message
flow was closed directly
What it means
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.
Source
Thrown at quic/v3/session.go:36
// A default is provided in the case that the client does not provide a close idle timeout.
defaultCloseIdleAfter = 210 * time.Second
// The maximum payload from the origin that we will be able to read. However, even though we will
// read 1500 bytes from the origin, we limit the amount of bytes to be proxied to less than
// this value (maxDatagramPayloadLen).
maxOriginUDPPacketSize = 1500
// The maximum amount of datagrams a session will queue up before it begins dropping datagrams.
// This channel buffer is small because we assume that the dedicated writer to the origin is typically
// fast enought to keep the channel empty.
writeChanCapacity = 512
logFlowID = "flowID"
logPacketSizeKey = "packetSize"
)
// SessionCloseErr indicates that the session's Close method was called.
var SessionCloseErr error = errors.New("flow was closed directly") //nolint:errname
// SessionIdleErr is returned when the session was closed because there was no communication
// in either direction over the session for the timeout period.
type SessionIdleErr struct { //nolint:errname
timeout time.Duration
}
func (e SessionIdleErr) Error() string {
return fmt.Sprintf("flow was idle for %v", e.timeout)
}
func (e SessionIdleErr) Is(target error) bool {
_, ok := target.(SessionIdleErr)
return ok
}
func newSessionIdleErr(timeout time.Duration) error {
return SessionIdleErr{timeout}View on GitHub (pinned to 2253eeeb25)
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.
Example fix
// before
if err := session.Serve(ctx); err != nil {
log.Error().Err(err).Msg("session failed")
}
// after
if err := session.Serve(ctx); err != nil && !errors.Is(err, v3.SessionCloseErr) && !errors.Is(err, v3.SessionIdleErr{}) {
log.Error().Err(err).Msg("session failed")
} Defensive patterns
Strategy: try-catch
Type guard
func isCleanSessionClose(err error) bool {
return errors.Is(err, v3.SessionCloseErr) || errors.Is(err, v3.SessionIdleErr{})
} Try / catch
if err := session.Serve(ctx); err != nil {
if errors.Is(err, v3.SessionCloseErr) || errors.Is(err, v3.SessionIdleErr{}) {
log.Debug().Msgf("flow closed: %s", err.Error())
return nil
}
return err
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- flow not found
- flow is in use by another connection
- flow is already registered for this connection
- flow registration rate limited
- invalid datagram type expected
AI-assisted analysis of cloudflare/cloudflared@2253eeeb25 (2026-09-06).
Data as JSON: /api/errors/3e144a3b7c8de91b.
Report an issue: GitHub.