Redirect to login instead of raw 401 JSON on expired session

A page tab left open past COOKIE_MAX_AGE and then reloaded hit
require_user/require_admin raising before the route body ever ran,
so there was nowhere to catch it and show something friendlier - the
browser just rendered {"detail": "Unauthorized"}. A global exception
handler now redirects GET page loads (never /api/* calls, whose JS
callers parse and handle the JSON error themselves, and never
POST/PUT/DELETE, so a failed login or CSRF check still reports its
own error instead of silently bouncing) with a 401/403 back to /.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-10-02 11:14:46 +00:00
parent 034f0b45ff
commit 008b99a85f
+18
View File
@@ -17,6 +17,7 @@ from markupsafe import Markup, escape
from sqlalchemy import delete, select, text, update
from sqlalchemy.orm import Session
from starlette.responses import HTMLResponse as _HR
from starlette.exceptions import HTTPException as _StarletteHTTPException
import urllib.request as _urllib_request
import urllib.parse as _urllib_parse
@@ -888,6 +889,23 @@ async def _process_callback_query(cq: dict):
app = FastAPI(title="MONT - инфрастуктурный полигон", docs_url=None, redoc_url=None, openapi_url=None)
@app.exception_handler(_StarletteHTTPException)
async def _auth_redirect_handler(request: Request, exc: _StarletteHTTPException):
"""A session that expired while a page tab sat open used to surface as
a raw {"detail": "Unauthorized"} JSON blob on reload - require_user/
require_admin raise before the page route body ever runs, so there was
nowhere in those routes to catch it and show something friendlier.
Redirect browser page loads (never /api/* calls, whose JS callers parse
the JSON body and show their own error) back to the login page instead."""
if (
exc.status_code in (401, 403)
and request.method == "GET"
and not request.url.path.startswith("/api/")
):
return RedirectResponse(url="/", status_code=303)
return JSONResponse(status_code=exc.status_code, content={"detail": exc.detail}, headers=exc.headers)
@app.get("/admin/access-request/{req_id}/decide")
def access_request_decide_confirm(req_id: str, action: str, token: str, db: Session = Depends(get_db)):
"""GET only renders a confirmation page - it must never mutate state,