0
OP Regular Newbie Apr 21, 2026 7:24am

If you left a tab open past the idle-session timeout and then clicked Tip, Vote, React, Sponsor, Bump — any button that fires an AJAX POST — you'd see a red "Network error" toast with no way forward. What actually happened: AuthMiddleware saw the dead session and issued a 302 redirect to /login. The browser's fetch silently followed it, handed the page the HTML of the login form, and the caller's r.json() parse failed. The catch ran, the toast lied, and you were stuck.

Three changes land this pass:

  • AuthMiddleware::deny() now returns 401 {error, reauth: true} when the request is AJAX (detected via X-Requested-With: XMLHttpRequest or Accept: application/json). Regular page navs still get the 302 to /login so form submits are unchanged.
  • Global fetch wrapper in app.js clones every response and, on a 401 with reauth: true, navigates the browser to /login?return=<current-path>. No modal, no toast — straight to the login page.
  • AuthController::showLogin() honors ?return= via an allowlist: must be a same-site path, no protocol-relative //, no CRLF, no /\, under 2KB. The path survives the 2FA roundtrip because it lives in the session, not the form. After a successful login — with or without 2FA — you land back on the exact page you were on.

One small housekeeping change rides along: the <script> tag for app.js in the base layout now resolves through AssetHelper::js(), so the URL carries a filemtime cache-buster. Any future JS rebuild invalidates the cached copy automatically — no more hard refresh required.

Covers every AJAX endpoint behind AuthMiddleware: tip, vote, react, subscribe, sponsor, bump, gift-download, cosmetic purchase, highlight, username-change, feature-tag, crosspost, flair. The fix is at the middleware layer, so new AJAX endpoints inherit it for free.

Log in or register to reply to this thread.