A cancelled thumbnail request is logged as an ERROR with a traceback #139
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Severity: MEDIUM — log noise that will hide real faults
Found while running the end-to-end suite for #98/#99/#100. The tests pass; the log does not.
What happens
Browsing the grid produces entries like this, at ERROR, with a full traceback:
Nothing is wrong. The browser cancelled an in-flight thumbnail request — by navigating away, or because the virtualized grid (#94) unmounted the
<img>before its bytes arrived. That is ordinary client behaviour, and #94 made it more ordinary by design: scrolling a large grid now unmounts images constantly.Why it happens
RequestLogMiddleware.dispatch(backend/app/middleware.py:77-93) wrapscall_nextin a bareexcept Exception:. Starlette'sBaseHTTPMiddlewareraisesRuntimeError("No response returned.")when the client goes away mid-response — the disconnect surfaces asanyio.EndOfStreaminside, and is re-raised as thatRuntimeError. The handler cannot tell it apart from a genuine unhandled fault, so it logs it as one and re-raises.Why it matters
This is #89's concern from the other side. That issue added logging so a real failure leaves a record someone can read; this defect fills the same log with tracebacks for something that is not a failure at all. A log where ERROR is routine is a log nobody reads, and the next genuine 500 arrives in a stream of identical-looking noise.
It is also load-bearing for triage:
get_ci_run_logsand any future alerting will key on ERROR.Scope
dispatch. Starlette'sClientDisconnect,anyio.EndOfStream, and theRuntimeError("No response returned.")re-raise all indicate the caller went away.#89built this for. A test should hold both halves apart.StreamingResponse) needs its own handling, since that is where a disconnect is most likely.Done when
References
backend/app/middleware.py:77-93Done in
2bb3530, CI green (run 65).A disconnect is recognised in the three shapes it can arrive in —
ClientDisconnect,anyio.EndOfStream, and theRuntimeErrorBaseHTTPMiddlewareraises when the application returned neither a response nor an exception — and logged at INFO with no stack. The stack was worth dropping rather than demoting: every one of them is the same three frames inside Starlette, so it carried no information at any level.The risk in a fix like this is that it swallows a real fault with the noise, so that is what most of the tests are about. Reading Starlette's
call_nextis what makes it safe rather than heuristic: a genuine exception from the application is re-raised in preference to the "no response" error, so the two cannot be confused by construction. Mutation-tested — making_caller_went_awayreturn true unconditionally fails four tests, one of them the pre-existingtest_an_unhandled_exception_is_logged_with_its_traceback.The one fragile part is that Starlette offers no typed signal for that case, only a message string. Rather than accept an untested coupling to someone else's wording, a test drives a real "no response" through the real middleware and compares it — so a Starlette upgrade that reworded it fails the suite instead of quietly restoring the noise, with every other test still passing.
It does not.
_streamcloses in afinally, so theGeneratorExitStarlette throws when it stops consuming runs it, and the handle is released. That is now a test rather than a reading of the code, because the failure it prevents — one leaked file handle per abandoned scroll, on a page #94 made scroll constantly — is invisible until a server runs out of them.Backend tests 1282 → 1291.