Found while implementing #187. Pre-existing, not caused by that work. NOT milestoned 1.0.0 — nothing in the tree flushes, hijacks or sets a write deadline today, so it is latent rather than live.
loggingResponseWriter in internal/middleware wraps http.ResponseWriter and implements no Unwrap() http.ResponseWriter. Since Go 1.20, http.ResponseController walks the chain via Unwrap to reach the underlying writer's optional interfaces — Flush, Hijack, SetReadDeadline, SetWriteDeadline. A wrapper without Unwrap terminates that walk, so ResponseController methods return http.ErrNotSupported for any handler behind this middleware, and the type-assertion route (w.(http.Flusher)) fails the same way.
Why it is worth fixing despite being latent: it fails silently and at a distance. The first handler that needs to stream — SSE for live event tailing, a chunked export, anything long-poll — will not fail at the wrapper. It will fail at the handler with a nil or ErrNotSupported, and the cause will be a middleware nobody was looking at. The fix is one method; the debugging session it prevents is not.
The recoverer added by #189 does implement Unwrap on its own writer wrapper, so it is not the gap — but that also means the chain is inconsistent, which is worse than uniformly wrong.
Everyhttp.ResponseWriter wrapper in the repo is audited for the same gap, not just this one — enumerate what you found, including the ones that already comply. A partial sweep here just relocates the trap.
A test driving http.ResponseController.Flush (or an equivalent optional interface) through the production middleware chain, not a hand-assembled one — the point is that the shipped chain works, and a hand-built stack is exactly how this was missed.
The mutation bites: removing the new Unwrap must fail that test.
Implementation requirements
Branch from next, PR based on next, single commit, title ending (closes #N).
Run make bootstrap in a fresh clone before gating — browser assets are fetched at build time, and make lint needs Docker.
Gate on make check plus the cache-defeated Docker lint path. All linting runs in Docker, never on the host. Clean up every container and image you start; never run any prune.
Found while implementing https://git.eeqj.de/sneak/webhooker/issues/187. Pre-existing, not caused by that work. NOT milestoned 1.0.0 — nothing in the tree flushes, hijacks or sets a write deadline today, so it is latent rather than live.
`loggingResponseWriter` in `internal/middleware` wraps `http.ResponseWriter` and implements no `Unwrap() http.ResponseWriter`. Since Go 1.20, `http.ResponseController` walks the chain via `Unwrap` to reach the underlying writer's optional interfaces — `Flush`, `Hijack`, `SetReadDeadline`, `SetWriteDeadline`. A wrapper without `Unwrap` terminates that walk, so `ResponseController` methods return `http.ErrNotSupported` for any handler behind this middleware, and the type-assertion route (`w.(http.Flusher)`) fails the same way.
Why it is worth fixing despite being latent: it fails **silently and at a distance**. The first handler that needs to stream — SSE for live event tailing, a chunked export, anything long-poll — will not fail at the wrapper. It will fail at the handler with a `nil` or `ErrNotSupported`, and the cause will be a middleware nobody was looking at. The fix is one method; the debugging session it prevents is not.
The recoverer added by https://git.eeqj.de/sneak/webhooker/pulls/189 does implement `Unwrap` on its own writer wrapper, so it is not the gap — but that also means the chain is inconsistent, which is worse than uniformly wrong.
## Definition of done
- `loggingResponseWriter` implements `Unwrap() http.ResponseWriter`.
- **Every** `http.ResponseWriter` wrapper in the repo is audited for the same gap, not just this one — enumerate what you found, including the ones that already comply. A partial sweep here just relocates the trap.
- A test driving `http.ResponseController.Flush` (or an equivalent optional interface) through the **production** middleware chain, not a hand-assembled one — the point is that the shipped chain works, and a hand-built stack is exactly how this was missed.
- The mutation bites: removing the new `Unwrap` must fail that test.
## Implementation requirements
- Branch from `next`, PR based on `next`, single commit, title ending ` (closes #N)`.
- Do not modify `TODO.md` (see https://git.eeqj.de/sneak/webhooker/issues/112).
- Run `make bootstrap` in a fresh clone before gating — browser assets are fetched at build time, and `make lint` needs Docker.
- Gate on `make check` plus the cache-defeated Docker lint path. All linting runs in Docker, never on the host. Clean up every container and image you start; never run any prune.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Found while implementing #187. Pre-existing, not caused by that work. NOT milestoned 1.0.0 — nothing in the tree flushes, hijacks or sets a write deadline today, so it is latent rather than live.
loggingResponseWriterininternal/middlewarewrapshttp.ResponseWriterand implements noUnwrap() http.ResponseWriter. Since Go 1.20,http.ResponseControllerwalks the chain viaUnwrapto reach the underlying writer's optional interfaces —Flush,Hijack,SetReadDeadline,SetWriteDeadline. A wrapper withoutUnwrapterminates that walk, soResponseControllermethods returnhttp.ErrNotSupportedfor any handler behind this middleware, and the type-assertion route (w.(http.Flusher)) fails the same way.Why it is worth fixing despite being latent: it fails silently and at a distance. The first handler that needs to stream — SSE for live event tailing, a chunked export, anything long-poll — will not fail at the wrapper. It will fail at the handler with a
nilorErrNotSupported, and the cause will be a middleware nobody was looking at. The fix is one method; the debugging session it prevents is not.The recoverer added by #189 does implement
Unwrapon its own writer wrapper, so it is not the gap — but that also means the chain is inconsistent, which is worse than uniformly wrong.Definition of done
loggingResponseWriterimplementsUnwrap() http.ResponseWriter.http.ResponseWriterwrapper in the repo is audited for the same gap, not just this one — enumerate what you found, including the ones that already comply. A partial sweep here just relocates the trap.http.ResponseController.Flush(or an equivalent optional interface) through the production middleware chain, not a hand-assembled one — the point is that the shipped chain works, and a hand-built stack is exactly how this was missed.Unwrapmust fail that test.Implementation requirements
next, PR based onnext, single commit, title ending(closes #N).TODO.md(see #112).make bootstrapin a fresh clone before gating — browser assets are fetched at build time, andmake lintneeds Docker.make checkplus the cache-defeated Docker lint path. All linting runs in Docker, never on the host. Clean up every container and image you start; never run any prune.clawbot referenced this issue2026-08-18 10:50:50 +02:00