go-chi-global-timeout-sse-stream-exemption
I diagnosed the issue, implemented the fix, and verified it with a real chi + httptest reproduction. The full write-up is in ~/solution.md.
Root cause. chi's middleware.Timeout wraps every request context in context.WithTimeout. SSE handlers select on r.Context().Done() to detect client disconnects, so the global cap is indistinguishable from a disconnect: at 60s the handler returns cleanly, the stream EOFs, and clients treat it as a normal end (no retry). middleware.Timeout has no per-route escape hatch, so a conditional wrapper is required.
Fix (verified as /tmp/sserepro/sse.go): a TimeoutWithSSEExemption(d) middleware that passes through only GET requests whose path ends in /events or /feed, and delegates everything else to the stock middleware.Timeout:
if r.Method == http.MethodGet && isSSEPath(r.URL.Path) {
next.ServeHTTP(w, r) // no deadline
return
}
capped.ServeHTTP(w, r) // ordinary requests keep the cap
Two deliberate choices: suffix matching (not an explicit path allowlist, which is how the MLS events surface got missed) and a method check (so POST .../events writes stay capped).
Verification (/tmp/sserepro/sse_test.go, all passing):
| Test | Plain middleware.Timeout |
With exemption |
|---|---|---|
| GET SSE stream (5 frames @100ms, 150ms cap) | frames=1 (RED) |
frames=5 (GREEN) |
POST /api/events |
— | 504 |
GET /api/slow |
— | 504 |
chi.Walk parity |
— | exempt set = /api/events, /api/feed, /mls/events, /federation/feed |
The parity test walks the real router with all conditional mounts enabled and pins the golden route set — it's what caught the missed 7th surface and the flag-gated federation routes. chi.Walk recurses into mounted sub-routers, so this works.
# Evidence - Problem class: go-chi-global-timeout-sse-stream-exemption - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-19T20:13:37.211Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "chi middleware.Timeout(60s) applied globally kills every SSE stream cleanly at the cap (handlers return on ctx.Done; clients treat the clean close as non-error and never retry). Solution that landed (hermes-canopy DF-33, judge PASS 06b4aacc): wrap the timeout in a conditional middleware that skips the context wrap for GET requests whose path ends in /events or /feed \u2014 all SSE routes share those suffixes \u2014 while ordinary requests keep the cap. Method check is mandatory (POST .../events writes must stay capped). Regression test: mini chi router with a 150ms timeout, real httptest transport, stream 5 frames at 100ms intervals (RED with plain middleware.Timeout: frames=1), ordinary route still gets 504. Router-walk parity test (chi.Walk on the real router, DB-free seam) pins the exempt route set and caught a missed 7th SSE surface (MLS events) plus conditionally-mounted federation routes.", "environment": "", "language": "", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "go-chi-global-timeout-sse-stream-exemption", "provider": "openrouter", "solved_at": "2026-09-19T20:13:37.211Z", "version": ""}I diagnosed the issue, implemented the fix, and verified it with a real chi + httptest reproduction. The full write-up is in ~/solution.md.
Root cause. chi's middleware.Timeout wraps every request context in context.WithTimeout. SSE handlers select on r.Context().Done() to detect client disconnects, so the global cap is indistinguishable from a disconnect: at 60s the handler returns cleanly, the stream EOFs, and clients treat it as a normal end (no retry). middleware.Timeout has no per-route escape hatch, so a conditional wrapper is required.
Fix (verified as /tmp/sserepro/sse.go): a TimeoutWithSSEExemption(d) middleware that passes through only GET requests whose path ends in /events or /feed, and delegates everything else to the stock middleware.Timeout:
if r.Method == http.MethodGet && isSSEPath(r.URL.Path) {
next.ServeHTTP(w, r) // no deadline
return
}
capped.ServeHTTP(w, r) // ordinary requests keep the cap
Two deliberate choices: suffix matching (not an explicit path allowlist, which is how the MLS events surface got missed) and a method check (so POST .../events writes stay capped).
Verification (/tmp/sserepro/sse_test.go, all passing):
| Test | Plain middleware.Timeout |
With exemption |
|---|---|---|
| GET SSE stream (5 frames @100ms, 150ms cap) | frames=1 (RED) |
frames=5 (GREEN) |
POST /api/events |
— | 504 |
GET /api/slow |
— | 504 |
chi.Walk parity |
— | exempt set = /api/events, /api/feed, /mls/events, /federation/feed |
The parity test walks the real router with all conditional mounts enabled and pins the golden route set — it's what caught the missed 7th surface and the flag-gated federation routes. chi.Walk recurses into mounted sub-routers, so this works.
# Evidence - Problem class: go-chi-global-timeout-sse-stream-exemption - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-19T20:13:37.211Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "chi middleware.Timeout(60s) applied globally kills every SSE stream cleanly at the cap (handlers return on ctx.Done; clients treat the clean close as non-error and never retry). Solution that landed (hermes-canopy DF-33, judge PASS 06b4aacc): wrap the timeout in a conditional middleware that skips the context wrap for GET requests whose path ends in /events or /feed \u2014 all SSE routes share those suffixes \u2014 while ordinary requests keep the cap. Method check is mandatory (POST .../events writes must stay capped). Regression test: mini chi router with a 150ms timeout, real httptest transport, stream 5 frames at 100ms intervals (RED with plain middleware.Timeout: frames=1), ordinary route still gets 504. Router-walk parity test (chi.Walk on the real router, DB-free seam) pins the exempt route set and caught a missed 7th SSE surface (MLS events) plus conditionally-mounted federation routes.", "environment": "", "language": "", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "go-chi-global-timeout-sse-stream-exemption", "provider": "openrouter", "solved_at": "2026-09-19T20:13:37.211Z", "version": ""}