{"record":{"id":"b75b5ddc744698f4","repo":"gofiber/fiber","slug":"errstreamclosed","errorCode":"errStreamClosed","errorMessage":"sse: stream closed","messagePattern":"sse: stream closed","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"warning","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/a105acad6c1e4576a77f01e02973f67e962bb58d/middleware/sse/sse.go#L3-L39","documentation":"The SSE handler tracks the stream's lifetime; once the client disconnects, the request context is canceled, or the handler returns, the stream is marked closed. Any subsequent write returns errStreamClosed to prevent writes on a dead connection from panicking or blocking.","triggerScenarios":"A goroutine spawned by the SSE handler attempts to write after the client has disconnected or the request context has been canceled.","commonSituations":"Background workers feeding events into a closed stream; generators that don't watch ctx.Done(); slow producers outliving the connection; reverse proxies timing out idle SSE connections.","solutions":["Select on ctx.Done() in every producer goroutine and stop writing when the context is canceled.","Treat errStreamClosed as a clean shutdown, not an error to retry or log as fatal.","Avoid spawning fire-and-forget goroutines tied to a single SSE connection; use a fan-out broker instead."],"exampleFix":"// before\ngo func() { for e := range events { sse.Write(e) } }()\n\n// after\ngo func() {\n    for {\n        select {\n        case <-ctx.Done():\n            return\n        case e := <-events:\n            if err := sse.Write(e); err != nil { return }\n        }\n    }\n}()","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"if err := sse.Write(ev); err != nil {\n    if errors.Is(err, sse.ErrStreamClosed) /* or the unexported sentinel via errors.Is */ {\n        return // clean shutdown\n    }\n    return err\n}","preventionTips":["Always select on ctx.Done() in producer goroutines.","Treat stream-closed as a normal exit, not a retryable error.","Use a fan-out broker instead of per-connection goroutines."],"tags":["sse","lifecycle","concurrency","context"],"backgroundTag":null,"analyzedSha":"a105acad6c1e4576a77f01e02973f67e962bb58d","analyzedAt":"2026-08-11T17:33:26.942Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}