Recover from a rejected cookie, and make non-persistent storage visible - #12
Merged
Merged
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #11, from the console output on the deployed app:
What
Invalid sessionactually meantThat 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_secretpersists a generated secret to<data_dir>/session_secretso it survives a restart. When the data directoryis 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_URLdefault to a SQLite file the appcannot keep, which is very likely the same root cause as the sessions that
"vanished", the QR code that 404'd, and the
Session not foundwe chasedseparately over several rounds. One unwritable mount, several unrelated-looking
symptoms.
Changes
Surface the condition —
GET /api/healthnow reportsstorage: "persistent" | "ephemeral", and the teacher dashboard shows a bannerwhen 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 cacheduser 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
participantsand
liveand spraying 401s at the console with nothing on screen explainingwhy.
Leave the page on logout —
logout()now navigates to/login. Staying ona 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:
versus the warning form.
Tests
61 backend, 5 frontend unit, 5 browser specs — all green.
Generated by Claude Code