fix(14.2): WR-02 499 only for a 5xx caused by the client going away

Every 5xx became 499 once the request context was cancelled, so a panic or
a real database error on a request the browser had abandoned passed the
phase gate's 5xx scan. Now a 5xx is logged as 499 only when the client had
already gone when the status was written and the handler did not panic;
recoverJSON and recoverBare mark the access log entry. Every record of an
abandoned request carries client_gone=true. The gate self-test plants a
5xx with client_gone=true and must refuse it.
This commit is contained in:
2026-10-12 11:19:12 +02:00
parent fbcfaaf05f
commit c30145ed25
6 changed files with 123 additions and 39 deletions

View File

@@ -60,8 +60,9 @@ Each request then writes one Info record with the message `http request` once it
|-----------|-------|
| `method` | The request method. |
| `pattern` | The matched route, method and path pattern, for example `GET /posts/{id}`. Empty when no route matched. |
| `status` | The response status; a handler that writes without setting one counts as 200. A 5xx written after the client cancelled the request is logged as 499 (client closed request), since the error came from the cancellation and no client received it. |
| `status` | The response status; a handler that writes without setting one counts as 200. A 5xx is logged as 499 (client closed request) only when the client had already gone away when the status was written and the handler did not panic: the error came from the cancellation, and no client received it. A 5xx written before the client went away keeps its status, and a recovered panic is always 500, so a search for 5xx records finds every real server error. |
| `duration_ms` | Time spent serving the request, in whole milliseconds. |
| `client_gone` | `true` when the client went away before the request was served (its context ended with `context.Canceled`); absent otherwise. |
The record never carries the URL path, the query string, request or response headers or the client address, because tokens often travel in paths and query strings. That holds for a request that reached no route too: a token route requested with another method gets a 405 from the router, and its trailing-slash variant a 404, before any route handler runs, so the path would carry the token. It goes to the published `*slog.Logger` or `slog.Default`, so the redacting handler still applies. Grep for the empty `pattern` to find requests that reached no route; the client's own log (a browser, a proxy) shows which path it asked for.