Fix timer action 500s after FastAPI upgrade

FastAPI 0.141 raises when response serialization hits a lazy-load
(current_block.subject.options) in async context, where 0.115 silently
skipped it. Eager-load Subject.options in session get/timer and the TV
dashboard queries. Pin pydantic 2.13.5.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
derekcandClaude Opus 5.5 committed 2026-09-24 23:51:10 -07:00
1 parent 4130467b22
commit 5a2510059d
5 files changed
+10 -4

No files matched your search

+2 -1
View File
@@ -7,7 +7,7 @@ Maintenance release from the 2026-09-24 review (`reports/maintenance-2026-09-24.
### Security ### Security
- Rate limits now apply to each client. nginx resolves the real client IP through Cloudflare → NPM (`set_real_ip_from` + `real_ip_recursive`). Before this, every request looked like it came from NPM, so all users shared one login limit. - Rate limits now apply to each client. nginx resolves the real client IP through Cloudflare → NPM (`set_real_ip_from` + `real_ip_recursive`). Before this, every request looked like it came from NPM, so all users shared one login limit.
- ntfy alerts show the real client IP (`X-Real-IP` from nginx). They no longer use the first `X-Forwarded-For` entry, which the client controls. - ntfy alerts show the real client IP (`X-Real-IP` from nginx). They no longer use the first `X-Forwarded-For` entry, which the client controls.
- Python dependency updates: - Python dependency updates (pydantic now pinned at 2.13.5):
- PyJWT 2.15.0 (fixes an auth bypass) - PyJWT 2.15.0 (fixes an auth bypass)
- anyio 4.15.1 - anyio 4.15.1
- fastapi 0.141.1 / starlette 1.7.0 - fastapi 0.141.1 / starlette 1.7.0
@@ -29,6 +29,7 @@ Maintenance release from the 2026-09-24 review (`reports/maintenance-2026-09-24.
- Docker log rotation on all services: json-file, 10 MB × 3. - Docker log rotation on all services: json-file, 10 MB × 3.
### Fixes ### Fixes
- **Timer actions returned 500** after the FastAPI 0.141 upgrade. The session queries didn't load `Subject.options`, which `DailySessionOut` serializes. Old FastAPI silently swallowed the failed lazy-load; the new one raises it. Now eager-loaded in `sessions.py` (get/timer) and `dashboard.py`.
- The meeting alert catch-up window fix (`8e92ae6`) is now deployed. - The meeting alert catch-up window fix (`8e92ae6`) is now deployed.
### Known / accepted ### Known / accepted
+1 -1
View File
@@ -39,7 +39,7 @@ async def get_dashboard(tv_token: int, db: AsyncSession = Depends(get_db)):
DailySession.session_date == date.today(), DailySession.session_date == date.today(),
DailySession.is_active == True, DailySession.is_active == True,
) )
.options(selectinload(DailySession.current_block)) .options(selectinload(DailySession.current_block).selectinload(ScheduleBlock.subject).selectinload(Subject.options))
.limit(1) .limit(1)
) )
session = session_result.scalar_one_or_none() session = session_result.scalar_one_or_none()
+2 -2
View File
@@ -172,7 +172,7 @@ async def get_session(
select(DailySession) select(DailySession)
.join(Child) .join(Child)
.where(DailySession.id == session_id, Child.user_id == current_user.id) .where(DailySession.id == session_id, Child.user_id == current_user.id)
.options(selectinload(DailySession.current_block).selectinload(ScheduleBlock.subject)) .options(selectinload(DailySession.current_block).selectinload(ScheduleBlock.subject).selectinload(Subject.options))
) )
session = result.scalar_one_or_none() session = result.scalar_one_or_none()
if not session: if not session:
@@ -191,7 +191,7 @@ async def timer_action(
select(DailySession) select(DailySession)
.join(Child) .join(Child)
.where(DailySession.id == session_id, Child.user_id == current_user.id) .where(DailySession.id == session_id, Child.user_id == current_user.id)
.options(selectinload(DailySession.current_block).selectinload(ScheduleBlock.subject)) .options(selectinload(DailySession.current_block).selectinload(ScheduleBlock.subject).selectinload(Subject.options))
) )
session = result.scalar_one_or_none() session = result.scalar_one_or_none()
if not session: if not session:
+1
View File
@@ -9,6 +9,7 @@ PyJWT==2.15.0
cryptography==50.0.1 cryptography==50.0.1
passlib[bcrypt]==1.7.4 passlib[bcrypt]==1.7.4
bcrypt==3.2.2 bcrypt==3.2.2
pydantic==2.13.5
pydantic-settings==2.5.2 pydantic-settings==2.5.2
alembic==1.20.0 alembic==1.20.0
Mako==1.4.3 Mako==1.4.3
+4
View File
@@ -156,6 +156,10 @@ This section was first blocked on backups. The user then asked for the HIGH find
| homeschool_backend | homeschool-backend | sha256:51b49afb4ec4… | | homeschool_backend | homeschool-backend | sha256:51b49afb4ec4… |
| homeschool_frontend | homeschool-frontend | sha256:f9a737699cda… | | homeschool_frontend | homeschool-frontend | sha256:f9a737699cda… |
### Post-deploy regression
After the HIGH deploy, `POST /api/sessions/{id}/timer` returned **500 on every call**: 8 failures, 0 successes, starting 06:43 UTC. FastAPI 0.141 raises on a failed lazy-load of `current_block.subject.options` during response serialization, where 0.115 silently skipped it. The first smoke test didn't cover timer actions. Fixed in v1.1.0 by eager-loading `Subject.options` in `sessions.py` and `dashboard.py`. The smoke test now covers the full timer lifecycle, the TV dashboard with an active block, schedule/subject writes, and every parameterless GET route (from the OpenAPI spec).
### Post-update scan (trivy HIGH/CRITICAL) ### Post-update scan (trivy HIGH/CRITICAL)
| Image | Before | After | | Image | Before | After |