fix(webapp): commit before the response, app-wide (#851) #854

Merged
claude-bot merged 3 commits from fix/851-commit-before-response into main 2026-09-26 20:01:24 +00:00
Collaborator

Closes #851.

The bug

In FastAPI 0.141.1's request_response, request-scoped generator dependencies are torn down after response(...) has sent the bytes. get_db commits in its teardown, so every route that left the commit to get_db answered before it committed.

That had two effects:

  • a client's next request could miss the write;
  • a commit that failed after the 200 was lost silently.

Before this fix, on main:

  • a vote PUT returned 200 while a second connection saw no vote;
  • a DELETE returned 204 with the row still there;
  • a failing commit still returned 204.

The fix

  • All 377 Depends(get_db) become Depends(get_db, scope="function"). FastAPI's own function scope tears down after the endpoint and serialisation, before the send.
    • It has to be every use, because the dependency cache key includes the scope. A mix would give one request two sessions.
    • Rollback on exception is unchanged.
    • A failed commit now returns 500 with the standard body.
    • The test harness keeps working, because overrides keep the scope.
  • Streaming: /record/stream already closed its session before looping, and /export streams from memory. There are no background tasks or WebSocket routes to worry about.
  • Votes:
    • the notification enqueue is now best-effort, logged and never failing the vote;
    • both vote routes commit before notify_vote_cast, so the bot's tally read can't be stale.

Tests (tests/test_commit_before_response.py, new)

  • The mechanism is pinned: function scope tears down before the send, and the default scope after.
  • A structural walk over every route: it fails if any get_db dependency isn't function-scoped. On main it reported 1,022 such dependencies. It asserts it found more than 300, so a broken walker can't pass silently.
  • Behavioural probes with the real get_db: a second connection checks at the moment the last byte is sent, for vote PUT, vote DELETE and a failing commit. All three failed on main.
  • test_discord_vote_symmetry: a failed .delay doesn't cost the vote, on both the web and bot paths, and the commit comes before the enqueue.
  • #845's regression test still passes.
  • Full suite: it ran once with no failures (4,719 collected). CI gives the exact counts.

For future code: every new Depends(get_db) must say scope="function", and the structural test enforces it. In-flight branches (lanes 6, 7 and 9) get converted when they merge.

🤖 Generated with Claude Code

Closes #851. ## The bug In FastAPI 0.141.1's `request_response`, request-scoped generator dependencies are torn down **after** `response(...)` has sent the bytes. `get_db` commits in its teardown, so every route that left the commit to `get_db` answered before it committed. That had two effects: - a client's next request could miss the write; - a commit that failed after the 200 was lost silently. Before this fix, on main: - a vote PUT returned 200 while a second connection saw no vote; - a DELETE returned 204 with the row still there; - a failing commit still returned 204. ## The fix - **All 377 `Depends(get_db)` become `Depends(get_db, scope="function")`.** FastAPI's own function scope tears down after the endpoint and serialisation, before the send. - It has to be every use, because the dependency cache key includes the scope. A mix would give one request two sessions. - Rollback on exception is unchanged. - A failed commit now returns 500 with the standard body. - The test harness keeps working, because overrides keep the scope. - **Streaming:** `/record/stream` already closed its session before looping, and `/export` streams from memory. There are no background tasks or WebSocket routes to worry about. - **Votes:** - the notification enqueue is now best-effort, logged and never failing the vote; - both vote routes commit before `notify_vote_cast`, so the bot's tally read can't be stale. ## Tests (`tests/test_commit_before_response.py`, new) - **The mechanism is pinned:** function scope tears down before the send, and the default scope after. - **A structural walk over every route:** it fails if any `get_db` dependency isn't function-scoped. On main it reported 1,022 such dependencies. It asserts it found more than 300, so a broken walker can't pass silently. - **Behavioural probes with the real `get_db`:** a second connection checks at the moment the last byte is sent, for vote PUT, vote DELETE and a failing commit. All three failed on main. - **`test_discord_vote_symmetry`:** a failed `.delay` doesn't cost the vote, on both the web and bot paths, and the commit comes before the enqueue. - **#845's regression test** still passes. - **Full suite:** it ran once with no failures (4,719 collected). CI gives the exact counts. **For future code:** every new `Depends(get_db)` must say `scope="function"`, and the structural test enforces it. In-flight branches (lanes 6, 7 and 9) get converted when they merge. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Fails on main. get_db commits in its teardown and, as a default-scoped
yield-dependency, FastAPI 0.141 tears it down after the response has been
sent. Pins the mechanism against the installed FastAPI, checks every
Depends(get_db) the app's routes reach, and runs the real get_db on a
real (slowed or failing) commit while a second connection looks at the
database as the response's last byte goes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
get_db commits in its teardown, and FastAPI 0.141 tears a yield-dependency
down at its default "request" scope after the response has been sent: a
route that left its commit to get_db answered before its write was
durable, and a commit that failed was logged after a 200.

Every Depends(get_db) is now scope="function", which FastAPI tears down
when the path operation (including response serialisation) returns and
before the response is sent: the commit lands first, and a failed one
propagates to the exception handlers as a 500 with the usual body. They
all change together because FastAPI caches a dependency per scope; a
mix would give one request two sessions. The streaming routes are
unaffected: the SSE stream already closes its session before it loops,
and the export streams an in-memory buffer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix(webapp): a vote stands when its notification can't be queued (#851)
All checks were successful
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m14s
CI / Bot tests and audit (pull_request) Successful in 2m5s
CI / Backend lint (ruff) (pull_request) Successful in 27s
CI / Feedback site tests (pull_request) Successful in 45s
CI / Bot/backend version sync (pull_request) Successful in 25s
CI / Docker image build (pull_request) Successful in 20s
CI / Does this change touch the recording path? (pull_request) Successful in 24s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been skipped
CI / Backend migration, tests, and audit (pull_request) Successful in 39m11s
CI / Frontend tests, audit, and build (pull_request) Successful in 46m25s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Successful in 19m6s
9a1f20a8d8
notify_vote_cast awaited the enqueue without catching a failure, though
the broker fails fast on the promise that a request-path enqueue is
non-fatal: a broker slow for a few seconds made a vote a 500 and rolled
it back (lane 6's J2, #732). The enqueue is now best-effort and logged.

Both vote routes also commit the vote before queueing its notification:
the bot answers a vote_update by reading the tally back from the API, so
a notification queued ahead of the commit could be answered without it.

CHANGELOG: one [webapp] Fixed bullet for #851.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rbrooks referenced this pull request from a commit 2026-09-26 20:01:46 +00:00
Sign in to join this conversation.
No description provided.