Session: 2026-02-14 10:51 — Search Pagination Production Bug Fix

Created: 2026-02-14 10:51 Updated: 2026-02-14 10:51 Notebook: Work/Sessions

Session: 2026-02-14 10:51 — Search Pagination Production Bug Fix

Summary

Investigated and fixed a production bug where search result pagination on give.berkeley.edu was broken — clicking "NEXT PAGE" loaded data but results never rendered. Also continued caching restoration work from the prior session (CSRF token migration, ac_param removal).

Work Completed

1. Caching Restoration (continued from prior session)

  • Removed ac_param context processor — dead code that triggered Vary: Cookie on every response
  • config/settings/base.py: removed from TEMPLATES context_processors
  • give/context_processors.py: removed ac_param function
  • Committed 632f6256, pushed to main

  • Migrated CSRF token from server-rendered to cookie-based — enables HTML page caching

  • templates/base.html: removed {{ csrf_token }} from hx-headers
  • give/static/assets/js/give/obf/global.js: added htmx:configRequest listener to read CSRF from cookie
  • Committed 4989c865, pushed to main, cherry-picked to qa

  • Fixed missing csrftoken cookie — after removing {{ csrf_token }}, Django stopped setting the cookie

  • give/base.py: added get_token(request) to HtmxTemplateView.dispatch() (checkout views only, non-cacheable)
  • Committed 6f816532, pushed to main, cherry-picked to qa

  • Regression testing: one-time gift ✓, recurring gift (6 tests) ✓, successive gifts (one-time → recurring) ✓

2. Production Search Pagination Bug (main work this session)

Symptom: On give.berkeley.edu, searching for "fund" loads page 1 correctly (759 results). Clicking "NEXT PAGE" updates pagination controls but results show "loading..." forever.

Diagnosis process: 1. Verified API works — curl to /api/searchresults?offset=10 returns valid JSON ✓ 2. Created diagnostic Playwright tests injecting instrumentation into Alpine store 3. Traced execution order: nextClicked() FINISHED before updateData() FINISHED — the await wasn't working 4. Key discovery: store.nextClicked.constructor.name === "Function" (NOT AsyncFunction) 5. store.nextClicked() returns undefined (NOT a Promise) — await was a no-op 6. Root cause confirmed: production nextClicked was a regular function, not async

Root cause: Three methods in searchResultsListener.js were not async: - nextClicked() — set loading=true, called updateData() fire-and-forget, loading never reset - previousClicked() — same issue - goToPage() — same issue (called from page input @focusout/@keyup.enter)

Prior fix was incomplete: Commit abe315fa (Jan 15) added async/await but removed manual page management, relying on updateCurrentPage() which has an off-by-one bug (Math.floor(offset/limit) returns 0-indexed page numbers). This fix was never deployed to prod or QA.

Corrected fix (commit d6f4703e): - Made all three methods async with proper await this.updateData() - Capture targetPage BEFORE calling updateData (which clobbers this.page via updateCurrentPage) - Set this.loading = false after data loads - Restored this.updatePager() calls (scroll-to-top)

Deployed to all branches: | Branch | Commit | Pipeline | |--------|--------|----------| | main | d6f4703e | ✓ pushed | | qa | 11e57676 | ✓ success | | prod | ae932263 | ✓ success | | mcp-server | d43373db | committed (not pushed) |

Post-deploy: Cloudflare was caching old JS (cf-cache-status: HIT, max-age: 1800). User purged CF cache manually. After purge, production test passed — forward (1→2→3) and backward (3→2→1) pagination all working.

3. New Test Files Created (not committed)

  • tests/navigation/search-pagination.spec.ts — full forward/backward pagination test
  • tests/checkout/successive-gifts.spec.ts — one-time gift followed by recurring gift in same session

Files Modified

File Change
give/static/assets/js/give/obf/searchResultsListener.js Fixed nextClicked, previousClicked, goToPage — async/await, page tracking, loading reset
give/context_processors.py Removed dead ac_param function (prior session work)
config/settings/base.py Removed ac_param from context_processors (prior session work)
templates/base.html Removed {{ csrf_token }} from hx-headers (prior session work)
give/static/assets/js/give/obf/global.js Added CSRF-from-cookie event listener (prior session work)
give/base.py Added get_token(request) to HtmxTemplateView.dispatch (prior session work)

Key Lessons

  1. JS obfuscation can mask bugs — production code was obfuscated; async keyword preserved for some methods but not others revealed the bug wasn't in the obfuscator but in the source
  2. updateCurrentPage() returns 0-indexed pagesMath.floor(offset/limit) for offset=10, limit=10 returns 1, not 2. Manual page tracking (this.page + 1) is necessary
  3. Cloudflare caches static assets for 30 min — after deploying JS fixes, must purge CF cache or wait for expiry
  4. Alpine.js async store methodsawait inside a non-async function is silently ignored; the function returns undefined instead of a Promise, making the await a no-op
  5. Diagnostic technique — injecting patched methods via page.evaluate() lets you test fixes against production without deploying

Pending Items

  • Remaining caching blockers: Vary: Cookie from AuthenticationMiddleware, Set-Cookie headers on cacheable pages
  • CACHING.md and CLOUDFLARE_CACHING_RESTORATION.md doc corrections
  • givecore December-2025 folder reorganization (staged but not committed in ~/Gitlab/givecore/)
  • Commit the two new test files (search-pagination.spec.ts, successive-gifts.spec.ts)