gofiber/fiber · warning

errStreamClosed

errStreamClosed

Error message

sse: stream closed

What it means

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.

Source

Thrown at middleware/sse/sse.go:21

// The package focuses on the SSE transport: response headers, wire formatting,
// flushing, heartbeat comments, and disconnect detection via flush errors.
// Application-specific concerns such as topics, replay storage, authentication,
// and pub/sub fan-out intentionally stay outside the core middleware.
package sse

import (
	"bufio"
	"context"
	"errors"
	"fmt"
	"sync"
	"time"

	"github.com/gofiber/fiber/v3"
	"github.com/gofiber/utils/v2"
)

var errStreamClosed = errors.New("sse: stream closed")

// New creates a new SSE handler.
func New(config ...Config) fiber.Handler {
	cfg := configDefault(config...)
	if cfg.Handler == nil {
		panic("sse: Handler must not be nil")
	}

	return func(c fiber.Ctx) error {
		c.Set(fiber.HeaderContentType, fiber.MIMETextEventStream)
		c.Set(fiber.HeaderCacheControl, "no-cache")
		c.Set(fiber.HeaderConnection, "keep-alive")
		c.Set("X-Accel-Buffering", "no")

		streamContext := c.Context()
		lastEventID := c.Get(fiber.HeaderLastEventID)

		c.Abandon()

View on GitHub (pinned to a105acad6c)

Solutions

  1. Select on ctx.Done() in every producer goroutine and stop writing when the context is canceled.
  2. Treat errStreamClosed as a clean shutdown, not an error to retry or log as fatal.
  3. Avoid spawning fire-and-forget goroutines tied to a single SSE connection; use a fan-out broker instead.

Example fix

// before
go func() { for e := range events { sse.Write(e) } }()

// after
go func() {
    for {
        select {
        case <-ctx.Done():
            return
        case e := <-events:
            if err := sse.Write(e); err != nil { return }
        }
    }
}()
Defensive patterns

Strategy: try-catch

Try / catch

if err := sse.Write(ev); err != nil {
    if errors.Is(err, sse.ErrStreamClosed) /* or the unexported sentinel via errors.Is */ {
        return // clean shutdown
    }
    return err
}

Prevention

When it happens

Trigger: A goroutine spawned by the SSE handler attempts to write after the client has disconnected or the request context has been canceled.

Common situations: 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.

Related errors


AI-assisted analysis of gofiber/fiber@a105acad6c (2026-08-11). Data as JSON: /api/errors/b75b5ddc744698f4. Report an issue: GitHub.