{"id":"06edfec35f45f467","repo":"gofiber/fiber","slug":"sse-stream-closed","errorCode":null,"errorMessage":"sse: stream closed","messagePattern":"sse: stream closed","errorType":"exception","errorClass":"errStreamClosed","httpStatus":null,"severity":"info","filePath":"middleware/sse/sse.go","lineNumber":21,"sourceCode":"// The package focuses on the SSE transport: response headers, wire formatting,\n// flushing, heartbeat comments, and disconnect detection via flush errors.\n// Application-specific concerns such as topics, replay storage, authentication,\n// and pub/sub fan-out intentionally stay outside the core middleware.\npackage sse\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/utils/v2\"\n)\n\nvar errStreamClosed = errors.New(\"sse: stream closed\")\n\n// New creates a new SSE handler.\nfunc New(config ...Config) fiber.Handler {\n\tcfg := configDefault(config...)\n\tif cfg.Handler == nil {\n\t\tpanic(\"sse: Handler must not be nil\")\n\t}\n\n\treturn func(c fiber.Ctx) error {\n\t\tc.Set(fiber.HeaderContentType, fiber.MIMETextEventStream)\n\t\tc.Set(fiber.HeaderCacheControl, \"no-cache\")\n\t\tc.Set(fiber.HeaderConnection, \"keep-alive\")\n\t\tc.Set(\"X-Accel-Buffering\", \"no\")\n\n\t\tstreamContext := c.Context()\n\t\tlastEventID := c.Get(fiber.HeaderLastEventID)\n\n\t\tc.Abandon()","sourceCodeStart":3,"sourceCodeEnd":39,"githubUrl":"https://github.com/gofiber/fiber/blob/9a4c7e57fe0b080a04235d28a4b0d2b4b353d58c/middleware/sse/sse.go#L3-L39","documentation":"Declared as errStreamClosed in sse/sse.go and returned by Stream.Event/Comment/Retry (via Stream.write) once the stream is marked isClosed. A stream closes either because a prior write/flush failed (failLocked sets isClosed and cancels the context) or because closeStream ran when the handler returned. It is a normal lifecycle signal that the client is gone or the stream is no longer writable, not a bug.","triggerScenarios":"Continuing to call stream.Event()/Comment()/Retry() after the client disconnected, after a heartbeat flush failed, or after the handler goroutine is winding down. Common when a producer goroutine keeps pushing events without observing Stream.Done()/Stream.Err().","commonSituations":"Long-lived SSE endpoints where the client closes the tab; mobile clients dropping connection; heartbeats failing through buffering proxies; producers that don't check stream state in a loop.","solutions":["Check stream.Err() / select on stream.Done() (or stream.Context().Done()) in your producer loop and stop writing when the stream ends.","Treat errStreamClosed (errors.Is) as a clean disconnect and exit the handler goroutine; do not log it as an error.","If using heartbeats, rely on the heartbeat goroutine to detect the disconnect via its own Comment() error."],"exampleFix":"// before\nfor _, ev := range events {\n    stream.Event(ev) // keeps writing after client gone\n}\n\n// after — stop when the stream ends\nfor _, ev := range events {\n    if err := stream.Event(ev); err != nil {\n        if errors.Is(err, sse.ErrStreamClosed) { break }\n        return err\n    }\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"// Check stream liveness before writing.\nfunc streamAlive(s *sse.Stream) bool {\n    select {\n    case <-s.Done():\n        return false\n    default:\n        return s.Err() == nil\n    }\n}","tryCatchPattern":"if err := stream.Event(ev); err != nil {\n    if errors.Is(err, sse.ErrStreamClosed) { // note: errStreamClosed is unexported\n        return nil // client disconnected — stop the producer\n    }\n    return err\n}","preventionTips":["In producer loops, select on stream.Done() / stream.Context().Done() and stop on disconnect.","Treat write errors after a disconnect as info, not errors.","Let the heartbeat goroutine detect disconnects if DisableHeartbeat is false."],"tags":["sse","stream","lifecycle","disconnect"],"analyzedSha":"9a4c7e57fe0b080a04235d28a4b0d2b4b353d58c","analyzedAt":"2026-08-04T21:44:03.395Z","schemaVersion":2}