The `held` channel in `runTwoRequests` was unbuffered, creating a race
condition with the `select/default` send in the handler. Under CI load
(slow runner, -race, -shuffle=on), the handler goroutine could reach
the select before the test goroutine blocked on `<-held`, causing the
send to silently fall through to `default` and deadlocking both
goroutines permanently.
Buffer the channel (capacity 1) so the send always succeeds regardless
of goroutine scheduling order.
* fix(server): prevent artwork throttle token starvation on slow clients
Replace Chi's ThrottleBacklog middleware for artwork endpoints with a
custom RequestThrottle that releases processing tokens before writing
the HTTP response. Previously, a slow or stalled client could hold a
throttle token indefinitely during io.Copy, exhausting all 2-4 slots
and blocking artwork requests for all users (reported after 15+ days
uptime).
The new approach buffers artwork into memory while holding the token,
releases it immediately, then writes the buffered response. A 30-second
per-request write deadline (SetWriteTimeout) prevents stalled writes
from blocking indefinitely. Throttle exhaustion is now logged with
context for operator visibility.
* refactor(server): simplify throttle to middleware with same API as Chi
Restructure RequestThrottle from a DI-injected type into a drop-in
middleware function with the same signature as Chi's ThrottleBacklog.
Handlers are reverted to their original simple form (no throttle
awareness), and the middleware is applied at route definition time
just like before. This eliminates the DI dependency, removes the
artworkThrottle field from both Router structs, and consolidates
SetWriteTimeout into the throttle file. When limit <= 0, the
middleware returns a passthrough so callers don't need a guard.
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(server): add opt-out flag for buffered artwork throttle
Add DevArtworkThrottleBuffered config (default true) that controls
whether the new buffered ThrottleBacklog middleware is used. When set
to false, it falls back to Chi's original middleware, giving users a
safety valve in case the buffered implementation causes issues.
Signed-off-by: Deluan <deluan@navidrome.org>
* test(server): clean up throttle tests for clarity and speed
Consolidate duplicate router setup into runTwoRequests() and
slowClientTest() helpers. Replace time.Sleep-based token holding with
channel synchronization, reducing suite time from ~7s to ~1.5s.
Remove redundant test, fix duplicate comment block, and add comment
explaining why slowTestWriter can't embed httptest.ResponseRecorder.
* fix: release artwork throttle tokens on panic
Defer the buffered artwork throttle release inside the handler closure so tokens are returned even when a downstream handler panics before response flushing. Document that the middleware buffers full responses in memory and add a regression test covering recovery after a panic.
* fix: align buffered throttle response behavior
Keep only the first status code written to the buffered artwork throttle response writer so it matches net/http semantics. Strengthen the opt-out test to verify DevArtworkThrottleBuffered=false uses Chi's original slow-client behavior instead of only checking shared 429 handling.
* refactor(server): remove setWriteTimeout from throttle middleware
SetWriteDeadline only constrains the server's Write syscall, not how
fast the client reads from the TCP buffer. For artwork-sized responses
(up to ~500KB), the kernel accepts the entire write immediately even
over real network interfaces due to TCP buffer auto-tuning. Verified
by testing with a stalled client over both loopback and en0 — the
deadline never triggers. The actual protection comes from buffering +
early token release, which is already in place.
---------
Signed-off-by: Deluan <deluan@navidrome.org>