From 008b99a85f9eef507810b1e89ccf6740aff23ad5 Mon Sep 17 00:00:00 2001 From: Ruslan Date: Fri, 2 Oct 2026 11:14:46 +0000 Subject: [PATCH] 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 --- app/main.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/app/main.py b/app/main.py index 8123f10..021011e 100644 --- a/app/main.py +++ b/app/main.py @@ -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,