Remove silent fallbacks found in the 2026-10-03 audit

- /api/ai/ask: the stub answer is reachable only under DEBUG or
  RAG_FORCE_MOCK. Without RAG keys on the server it used to return 200 with
  "this is a stub, set OPENAI_API_KEY" and spend the student's quota; now 503.
- /api/home: the `grade` alias is gone and sending it is a 400 instead of a
  silent fallback to the user's own grade. No client sent it.
- activate_subscription logs ERROR when the paid order's plan no longer exists
  (it silently became 30 days).
- resolve_grade_title logs instead of `except: pass`; curriculum_io no longer
  reads an `intro_text` key no file contains.
- CLAUDE.md: "No silent fallbacks" rules for future agents.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 10:38:42 +03:30
parent 7e4b8b54a6
commit 7307bfcadb
10 changed files with 98 additions and 24 deletions

View File

@@ -22,6 +22,47 @@ at checkout, `select_for_update` guards that SQLite silently ignores).
race-free / the right architecture"; write "guards X; not tested under Y" instead.
Record uncertainty plainly so the next reader re-checks rather than trusts.
## No silent fallbacks, no speculative backward compatibility
Agents keep adding "helpful" fallbacks (`x or default`, `except: pass`, accepting an old
field name "just in case") and they cause the hardest bugs here, because the
failure never shows up anywhere. An audit on 2026-10-03 found and reproduced these:
- With RAG keys missing from `.env` and `DEBUG=0`, `/api/ai/ask` answered **200** with
«این یک پاسخ نمونه (stub)… OPENAI_API_KEY را تنظیم کنید» and still used up the
student's daily quota. It now returns 503 (`StubNeverReachesProductionTests`).
- A paid order whose plan had been deleted quietly became 30 days (a yearly buyer got
a month). It still activates, but now logs ERROR.
- `?grade=` on `/api/home` was an alias "for clients already out there". No such client
existed. Removed, and sending it is a 400, not a silent fallback to the user's own grade.
- `resolve_grade_title` had `except Exception: pass` (now logged); `curriculum_io` read
`intro_text` that no file ever contains (removed).
Rules:
1. **Fail loudly over guessing.** A missing setting, row or field in production should
be an error the user and `/api/health` can see, not a default that looks like
success. A default is fine only when it is a real product choice. When you
add one, say in the comment why that value is right, not "just in case".
2. **Demo/stub paths only behind `DEBUG` or `RAG_FORCE_MOCK`.** Never reachable on the server.
3. **No backward compatibility without a named client.** Before keeping an old field, param
or code path, check who sends it (`~/Desktop/MathApp_frontend/src/api/`, the server
DB). The only API client is that frontend; there is no Android app yet. If nobody
sends it, delete it. If you must keep one, write who uses it and when it can go.
4. **Never `except: pass`.** At least `logger.exception`. Swallowing is acceptable only
where the comment names the bigger thing it protects (e.g. `affiliate/services.py`:
commission failure must not undo the user's payment) and it still logs.
5. **Removing a fallback means making the old input fail visibly** (400/503), not
silently routing it somewhere else.
Fallbacks that remain on purpose (re-check before relying on them): settings boot with
default hosts/CORS/cache/secret key (`/api/health` warns); `LocMemCache` without
`CACHE_URL`; `_annotation` defaults for answer-cache entries older than a field; Bazaar's
OAuth path beside the Pishkhan token (server still uses OAuth); `birthYear` alone in
`PATCH /api/user/profile` (frontend type still allows it); `totalQuestions` accepted but
ignored (frontend sends it); orders with no `plan_id` from before 2026-09-25 (the
server DB was not checked for any).
## Before any real user: read `docs/security-before-production.md`
The server is deliberately left in demo mode for the internship presentation: the token