fix(search): stop replaying a failed LA-averages request forever #178

Merged
tudor merged 1 commits from fix/la-average-cached-failure into main 2026-10-02 22:44:57 +00:00
Owner

The "vs LA avg" comparison on secondary search rows disappeared after the #175/#176 deploy. Neither PR touched it. The cause is older.

Root cause

HomeView fetched /api/la-averages with cache: 'force-cache'. That mode serves any stored response, however old, without asking the server. One failed request was stored and replayed on every later visit, and the .catch(() => {}) hid it, so laAverages stayed empty and no row showed a delta. Failures that can get stored this way include a deploy restart, when the proxy answers 502, and the July proxy outage.

Evidence from a Playwright browser profile on staging:

fetch mode status time Date
force-cache (what the app did) 500 2 ms Sun, 05 Jul 2026
no-store 200, 153 LAs 60 ms today
default (this fix) 200, 153 LAs 82 ms, then 2 ms from cache today

After the default-mode fetch replaced the stored entry, the same page showed "49.4 Attainment 8 +11.4 vs LA avg" for Burntwood. The delta code on staging was fine all along.

Fix

Use the default cache mode. The API already sends Cache-Control: public, max-age=300, stale-while-revalidate=604800 (and only on 200s), so a good answer is reused for five minutes and an error never is. Browsers that hold a stored failure recover on their next visit after this deploys, with nothing to clear.

Tests

  • Jest: HomeView.staleFetch.test.tsx checks LA averages are not fetched with force-cache. It fails before the fix. 70 suites, 597 tests pass; tsc --noEmit clean.
  • E2E: a mainstream secondary's search row shows "vs LA avg". Nothing checked this before, which is how it went missing unnoticed. Playwright disables the HTTP cache while intercepting requests, so a journey can't replay a stored failure. The unit test pins the cache mode instead. Passes against staging.
  • Merges cleanly with #177 (checked with git merge-tree).

🤖 Generated with Claude Code

The "vs LA avg" comparison on secondary search rows disappeared after the #175/#176 deploy. Neither PR touched it. The cause is older. ## Root cause `HomeView` fetched `/api/la-averages` with `cache: 'force-cache'`. That mode serves **any stored response, however old, without asking the server**. One failed request was stored and replayed on every later visit, and the `.catch(() => {})` hid it, so `laAverages` stayed empty and no row showed a delta. Failures that can get stored this way include a deploy restart, when the proxy answers 502, and the July proxy outage. Evidence from a Playwright browser profile on staging: | fetch mode | status | time | `Date` | |---|---|---|---| | `force-cache` (what the app did) | **500** | 2 ms | **Sun, 05 Jul 2026** | | `no-store` | 200, 153 LAs | 60 ms | today | | default (this fix) | 200, 153 LAs | 82 ms, then 2 ms from cache | today | After the default-mode fetch replaced the stored entry, the same page showed **"49.4 Attainment 8 +11.4 vs LA avg"** for Burntwood. The delta code on staging was fine all along. ## Fix Use the default cache mode. The API already sends `Cache-Control: public, max-age=300, stale-while-revalidate=604800` (and only on 200s), so a good answer is reused for five minutes and an error never is. Browsers that hold a stored failure recover on their next visit after this deploys, with nothing to clear. ## Tests - Jest: `HomeView.staleFetch.test.tsx` checks LA averages are not fetched with `force-cache`. It fails before the fix. 70 suites, 597 tests pass; `tsc --noEmit` clean. - E2E: a mainstream secondary's search row shows "vs LA avg". Nothing checked this before, which is how it went missing unnoticed. Playwright disables the HTTP cache while intercepting requests, so a journey can't replay a stored failure. The unit test pins the cache mode instead. Passes against staging. - Merges cleanly with #177 (checked with `git merge-tree`). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
tudor added 1 commit 2026-10-02 22:20:27 +00:00
fix(search): stop replaying a failed LA-averages request forever
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m12s
PR Checks / Backend Smoke (pull_request) Successful in 9s
PR Checks / Build Backend (no push) (pull_request) Successful in 18s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m18s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 11s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 18s
59ea8a4bdd
The search page fetched LA averages with cache: 'force-cache', which serves
any stored response, however old, without asking the server. One failed
request (a staging deploy restart; the July proxy outage) was stored and
replayed on every later visit, and the error was swallowed, so the
"vs LA avg" delta silently vanished from every secondary row in that
browser. A Playwright profile still held a 500 dated 5 July.

The default cache mode honours the API's Cache-Control (five minutes), so
a good answer is still reused and an error never is. Browsers holding a
stored failure recover on their next visit.

A journey now checks that a mainstream secondary's row shows the
comparison: nothing did, which is how it could go missing unnoticed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

🤖 AI Code Review (Claude Code)

Removes the force-cache mode from the LA-averages fetch in HomeView so a single failed response is no longer replayed from the browser cache. Adds a unit test pinning the cache mode and an e2e journey checking that a secondary search row shows the 'vs LA avg' comparison. The change is small and correct, with only minor test-robustness concerns.

🟡 Minor

  • nextjs-app/components/HomeView.tsx: fetchLAaverages() is now called with no options. If the helper in lib/api reads an options argument without a default (e.g. destructures it), this would throw. The unit test reads options?.cache, which suggests it is optional, but this is worth confirming. The helper may also set its own cache mode internally, which the unit test would not catch.
  • nextjs-app/tests/components/HomeView.staleFetch.test.tsx: The test only asserts that the cache option is not 'force-cache'. It would pass if a different problematic mode such as 'only-if-cached' were used. It also assumes fetchLAaverages is already mocked in this file; if the mock is not set up, jest.mocked(...).mock.calls will fail.
  • e2e/tests/journeys.spec.ts: The e2e test depends on CSS-module class name fragments (__rowContent, __line3) and on live data (a secondary school matching the search 'school' that has an LA average). The fragments make it brittle against styling refactors. The test.skip fallback means the guard can silently become a no-op. The comment also says the test guards the comparison, but it cannot detect the force-cache regression, since Playwright disables the HTTP cache.
## 🤖 AI Code Review (Claude Code) Removes the `force-cache` mode from the LA-averages fetch in HomeView so a single failed response is no longer replayed from the browser cache. Adds a unit test pinning the cache mode and an e2e journey checking that a secondary search row shows the 'vs LA avg' comparison. The change is small and correct, with only minor test-robustness concerns. ### 🟡 Minor - **nextjs-app/components/HomeView.tsx**: fetchLAaverages() is now called with no options. If the helper in lib/api reads an options argument without a default (e.g. destructures it), this would throw. The unit test reads `options?.cache`, which suggests it is optional, but this is worth confirming. The helper may also set its own cache mode internally, which the unit test would not catch. - **nextjs-app/__tests__/components/HomeView.staleFetch.test.tsx**: The test only asserts that the cache option is not 'force-cache'. It would pass if a different problematic mode such as 'only-if-cached' were used. It also assumes fetchLAaverages is already mocked in this file; if the mock is not set up, `jest.mocked(...).mock.calls` will fail. - **e2e/tests/journeys.spec.ts**: The e2e test depends on CSS-module class name fragments (`__rowContent`, `__line3`) and on live data (a secondary school matching the search 'school' that has an LA average). The fragments make it brittle against styling refactors. The `test.skip` fallback means the guard can silently become a no-op. The comment also says the test guards the comparison, but it cannot detect the force-cache regression, since Playwright disables the HTTP cache.
tudor merged commit 1c62e8247d into main 2026-10-02 22:44:57 +00:00
Sign in to join this conversation.
No Reviewers
No labels
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: tudor/school_compare#178