sse: real-time flushing via raw handler overriding the generated route

The generated strict-server path could only return an io.Reader that
io.Copy drains without flushing, so SSE events sat chunk-buffered instead
of streaming in real time. Replace it with a raw http.ResponseWriter
handler (serveSSE) that Flush()es after every event.

Routing: chi allows a later registration to supersede an earlier one for
the same method+path (verified empirically for v5.3.1), so serveSSE is
registered on the router AFTER gen.HandlerWithOptions and wins over the
generated /events/stream route. It inherits the base middleware chain and
applies auth via With(). The generated StreamEvents method now returns an
error (never reached) so a routing regression fails loudly rather than
silently reverting to buffered delivery.

Adds TestSSEStreamRealtimeDelivery: a real httptest.NewServer + streaming
client (NewRecorder can't flush) that connects, triggers an event, and
asserts delivery within 3s — proving both the override routing and
per-event flushing. Passes in <1s.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-07 13:34:32 +02:00
parent f1b0b65149
commit 7e802bbb14
3 changed files with 142 additions and 45 deletions

View File

@@ -101,12 +101,16 @@ func NewHandler(ctx context.Context, pool *db.Pool, cfg config.Config) http.Hand
},
})
// SSE stream: override the generated /events/stream route with a raw
// flushing handler (registered AFTER HandlerWithOptions so chi's last
// registration wins). The strict-server path can't Flush() per event;
// this one uses the real ResponseWriter for real-time delivery. It
// inherits the router's base middleware and applies auth via With().
r.With(combinedAuth(cfg)).Get("/api/v1/events/stream", s.serveSSE)
// Mount MCP at /mcp (plan R3-10)
r.With(combinedAuth(cfg)).Handle("/mcp", mcphandler.NewHandler(pool, cfg.MCPBearerToken))
// Mount SSE stream — handled by the strict handler's StreamEvents method
// via the OpenAPI-specified /api/v1/events/stream route.
return r
}