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 import delete, select, text, update
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from starlette.responses import HTMLResponse as _HR from starlette.responses import HTMLResponse as _HR
from starlette.exceptions import HTTPException as _StarletteHTTPException
import urllib.request as _urllib_request import urllib.request as _urllib_request
import urllib.parse as _urllib_parse 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 = 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") @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)): 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, """GET only renders a confirmation page - it must never mutate state,