Skip to content

Recover from a rejected cookie, and make non-persistent storage visible - #12

Merged
percolator merged 3 commits into
mainfrom
claude/bioinformatics-quiz-app-lpr8hz
Aug 3, 2026
Merged

Recover from a rejected cookie, and make non-persistent storage visible#12
percolator merged 3 commits into
mainfrom
claude/bioinformatics-quiz-app-lpr8hz

Conversation

@percolator

Copy link
Copy Markdown
Contributor

Follow-up to #11, from the console output on the deployed app:

GET /api/sessions/<code>/participants  401  {detail: 'Invalid session'}
GET /api/sessions/<code>/qr.svg        401  {detail: 'Invalid session'}

What Invalid session actually meant

That message is raised in exactly one place: the cookie's signature failed to
verify. The cookie was not expired and not absent — it was signed with a
different secret than the one the running process holds.

Settings.resolved_session_secret persists a generated secret to
<data_dir>/session_secret so it survives a restart. When the data directory
is not writable it cannot do that, and falls back to a fresh random secret
per process. So every restart silently invalidated every cookie.

The same condition makes DATABASE_URL default to a SQLite file the app
cannot keep, which is very likely the same root cause as the sessions that
"vanished", the QR code that 404'd, and the Session not found we chased
separately over several rounds. One unwritable mount, several unrelated-looking
symptoms.

Changes

Surface the conditionGET /api/health now reports
storage: "persistent" | "ephemeral", and the teacher dashboard shows a banner
when it is ephemeral. Startup logging already warned about it; this makes it
checkable from outside the container, which matters on a platform where reading
logs is awkward.

Recover instead of looping — new authErrorInterceptor
(frontend/src/app/auth.interceptor.ts) turns any 401 into "forget the cached
user and go to /login", except on /api/auth/ probes where a 401 is the normal
"not logged in" answer and would otherwise cause a redirect loop. Before this,
a rejected cookie left the teacher on a view that kept polling participants
and live and spraying 401s at the console with nothing on screen explaining
why.

Leave the page on logoutlogout() now navigates to /login. Staying on
a teacher view after signing out left the same polling running against
endpoints that now answer Not logged in.

The operational fix is separate

This PR makes the failure legible; it does not create the volume. If the banner
appears on the deployed app, the mount is not writable and answers are being
lost on every restart. The startup log line to look for is:

data dir /home/data is writable

versus the warning form.

Tests

61 backend, 5 frontend unit, 5 browser specs — all green.


Generated by Claude Code

claude added 3 commits August 3, 2026 16:54
Diagnosis from a report of "Session not found." with a broken QR code: every
authenticated request was returning 401 {"detail": "Invalid session"}. That
is raised only when the cookie's signature does not verify — the user is
effectively signed out while the page still looks signed in.

The likely cause is the session secret changing. It is generated per process
whenever no writable data directory is mounted, so every restart invalidates
every cookie. The same condition makes the database ephemeral, which explains
sessions that had vanished earlier.

Two changes so this cannot present as a mystery again.

An HTTP interceptor sends the user back to the login page on any 401, instead
of leaving each view to render its own interpretation of a failed request —
"Session not found.", a broken image — while the header still shows them as
logged in. Requests to /api/auth/ are exempt: a 401 there is the answer to the
question being asked, not a stale session.

/api/health now reports whether the data directory is writable, and the
teacher dashboard shows a warning when it is not. Non-persistent storage does
not announce itself: the app works until it restarts, then the quizzes are
gone and everyone is signed out, with nothing on screen connecting the two.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018u3wNzXNdrPw2Z59WjGHYv
Signing out cleared the session but stayed on whatever teacher view was open,
which kept polling participants and the live count against endpoints that now
answer 401 — a stream of console errors and no indication of what happened.
Logout now returns to the login page, and navigates there even if the logout
request itself fails, since the local session is gone either way.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018u3wNzXNdrPw2Z59WjGHYv
The Docker build and the e2e serve script both copy the built Angular app
to backend/static so a single origin serves the API and the SPA. It is
generated output: committing it would leave a stale bundle in the tree that
shadows the real build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018u3wNzXNdrPw2Z59WjGHYv
@percolator
percolator merged commit 335f045 into main Aug 3, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants