You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Thanks to @pree-dew (Pree) for reporting these log lines at the last Registry Working Group meeting. 🙏
Summary
When a client disconnects mid-request on GET /v0/servers, the list handler does not detect the cancellation. It (a) logs the resulting context.Canceled at default level as list servers failed: ..., and (b) returns huma.Error500InternalServerError, which makes huma attempt to write a status to an already-committed/closed response — producing a superfluous response.WriteHeader call warning. Both are benign-but-noisy: no real client is affected, because the client has already gone away.
Observed log lines (production)
2026/05/28 20:28:26 list servers failed: error iterating rows: context canceled
... superfluous response.WriteHeader call from github.com/danielgtaylor/huma/v2/adapters/humago.(*goContext).SetStatus (humago.go:110)
Root cause
internal/api/handlers/v0/servers.go (~line 135) logs and 500s on every error from the service, with no special-casing for client cancellation:
servers, nextCursor, err:=registry.ListServers(ctx, filter, input.Cursor, input.Limit)
iferr!=nil {
log.Printf("list servers failed: %v", err)
returnnil, huma.Error500InternalServerError("Failed to get registry list")
}
The error string originates from internal/database/postgres.go (~line 262), fmt.Errorf("error iterating rows: %w", err), where rows.Err() returns context.Canceled when the client disconnects during row streaming. Returning a huma 5xx here then drives humago's SetStatus (adapters/humago/humago.go:110, which calls w.WriteHeader unguarded) to write a status onto a response whose header is already committed → the superfluous response.WriteHeader call warning.
So the two log lines are two symptoms of one root cause: the list handler does not short-circuit on client cancellation.
Affected files
internal/api/handlers/v0/servers.go (≈ line 135) — unconditional error log + 500
internal/database/postgres.go (≈ line 262) — error origin (the same error iterating rows pattern also appears at ≈ lines 479 and 722 and shares the exposure)
internal/service/registry_service.go (≈ line 55) — pass-through
Relationship to existing work
metrics: stop counting client-cancelled requests as server errors #1255 (merged) fixed only the metrics side — it remaps cancelled 500→499 in internal/api/router/router.go and skips http_errors_total. Its description explicitly notes handlers "still call huma.Error500… on context.Canceled" and "tried to write a response to a closed socket", i.e. the handler-side logging and double-write were deliberately left out of scope.
API latency: /v0/servers reads consistently take 20–25s #1252 (open) tracks the underlying /v0/servers latency that causes clients to time out and cancel in the first place. This issue is about the noisy logging/response behavior and is independent of the latency fix.
A regression test already associates this exact error string with cancellation: internal/api/router/router_test.go's TestMetricMiddleware_ClientCancelledNotCountedAsError simulates it with errors.New("error iterating rows: context canceled").
Suggested fix direction
Detect cancellation in the list handler before logging an error or returning a 5xx, e.g.:
iferr!=nil {
iferrors.Is(err, context.Canceled) ||ctx.Err() !=nil {
// client went away — nothing to return; skip the noisy error log and 5xxreturnnil, huma.Error499ClientClosedRequest(...) // or return the context error without error-level logging
}
log.Printf("list servers failed: %v", err)
returnnil, huma.Error500InternalServerError("Failed to get registry list")
}
Returning early (without invoking the error-response writer) also prevents the second WriteHeader. Consider applying the same guard to the other handlers that call streaming db.* methods, and/or downgrading cancellation to a debug-level log. A small regression test mirroring the existing TestMetricMiddleware_ClientCancelledNotCountedAsError could assert that no error-level log / no 5xx is produced on cancellation.
Reproduction
make dev-compose, then curl --max-time 0.05 http://localhost:8080/v0/servers (or start the request and abort it). Both log lines appear on the server.
Severity
Low — log noise only; no client-visible incorrect behavior. Worth fixing to keep stderr/alerting signal clean and to avoid masking genuine double-write bugs.
Thanks to @pree-dew (Pree) for reporting these log lines at the last Registry Working Group meeting. 🙏
Summary
When a client disconnects mid-request on
GET /v0/servers, the list handler does not detect the cancellation. It (a) logs the resultingcontext.Canceledat default level aslist servers failed: ..., and (b) returnshuma.Error500InternalServerError, which makes huma attempt to write a status to an already-committed/closed response — producing asuperfluous response.WriteHeader callwarning. Both are benign-but-noisy: no real client is affected, because the client has already gone away.Observed log lines (production)
Root cause
internal/api/handlers/v0/servers.go(~line 135) logs and 500s on every error from the service, with no special-casing for client cancellation:The error string originates from
internal/database/postgres.go(~line 262),fmt.Errorf("error iterating rows: %w", err), whererows.Err()returnscontext.Canceledwhen the client disconnects during row streaming. Returning a huma 5xx here then driveshumago'sSetStatus(adapters/humago/humago.go:110, which callsw.WriteHeaderunguarded) to write a status onto a response whose header is already committed → thesuperfluous response.WriteHeader callwarning.So the two log lines are two symptoms of one root cause: the list handler does not short-circuit on client cancellation.
Affected files
internal/api/handlers/v0/servers.go(≈ line 135) — unconditional error log + 500internal/database/postgres.go(≈ line 262) — error origin (the sameerror iterating rowspattern also appears at ≈ lines 479 and 722 and shares the exposure)internal/service/registry_service.go(≈ line 55) — pass-throughRelationship to existing work
500→499ininternal/api/router/router.goand skipshttp_errors_total. Its description explicitly notes handlers "still callhuma.Error500…oncontext.Canceled" and "tried to write a response to a closed socket", i.e. the handler-side logging and double-write were deliberately left out of scope./v0/serverslatency that causes clients to time out and cancel in the first place. This issue is about the noisy logging/response behavior and is independent of the latency fix.A regression test already associates this exact error string with cancellation:
internal/api/router/router_test.go'sTestMetricMiddleware_ClientCancelledNotCountedAsErrorsimulates it witherrors.New("error iterating rows: context canceled").Suggested fix direction
Detect cancellation in the list handler before logging an error or returning a 5xx, e.g.:
Returning early (without invoking the error-response writer) also prevents the second
WriteHeader. Consider applying the same guard to the other handlers that call streamingdb.*methods, and/or downgrading cancellation to a debug-level log. A small regression test mirroring the existingTestMetricMiddleware_ClientCancelledNotCountedAsErrorcould assert that no error-level log / no 5xx is produced on cancellation.Reproduction
make dev-compose, thencurl --max-time 0.05 http://localhost:8080/v0/servers(or start the request and abort it). Both log lines appear on the server.Severity
Low — log noise only; no client-visible incorrect behavior. Worth fixing to keep stderr/alerting signal clean and to avoid masking genuine double-write bugs.