From 40e0db8cd8c034b51f6d93d6b2cc3e011d135989 Mon Sep 17 00:00:00 2001 From: Mng <50384638+Mng-dev-ai@users.noreply.github.com> Date: Sun, 22 Feb 2026 03:00:20 +0200 Subject: [PATCH 1/2] Fix CORS headers missing on error responses in Tauri desktop app MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move CORSMiddleware to be the outermost middleware so CORS headers are always present — even on 500s from inner middleware. Previously, error responses bypassed CORS, causing WebKit to block them entirely and preventing the frontend from reading the status code to invalidate stale sessions. Also simplify shouldInvalidateSession to a single expression. --- backend/app/core/middleware.py | 6 +++--- frontend/src/lib/api.ts | 15 +++------------ 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/backend/app/core/middleware.py b/backend/app/core/middleware.py index 83bdbae..fbe032d 100644 --- a/backend/app/core/middleware.py +++ b/backend/app/core/middleware.py @@ -149,6 +149,9 @@ async def _global_exception_handler(request: Request, exc: Exception) -> JSONRes def setup_middleware(app: FastAPI) -> None: + session_secret = settings.SESSION_SECRET_KEY or settings.SECRET_KEY + app.add_middleware(SessionMiddleware, secret_key=session_secret) + app.add_middleware(SecurityHeadersMiddleware) app.add_middleware(RequestIdMiddleware) @@ -168,9 +171,6 @@ def setup_middleware(app: FastAPI) -> None: expose_headers=["X-Message-Id", "X-Request-ID", "X-Process-Time"], ) - session_secret = settings.SESSION_SECRET_KEY or settings.SECRET_KEY - app.add_middleware(SessionMiddleware, secret_key=session_secret) - app.add_exception_handler(ServiceException, _service_exception_handler) app.add_exception_handler(StarletteHTTPException, _http_exception_handler) app.add_exception_handler(Exception, _global_exception_handler) diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 83657fe..81d47cb 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -87,19 +87,10 @@ async function refreshTokenIfNeeded(baseURL: string): Promise { } } +// Desktop reinstalls can leave stale refresh tokens in local storage while the backend +// identity/session store resets. Treat refresh 4xx as terminal to break retry loops. function shouldInvalidateSession(error: unknown): boolean { - if (!(error instanceof RefreshTokenError)) { - return false; - } - if (error.status === 0) { - return false; - } - if (error.status >= 500) { - return false; - } - // Desktop reinstalls can leave stale refresh tokens in local storage while the backend - // identity/session store resets. Treat refresh 4xx as terminal to break retry loops. - return error.status >= 400 && error.status < 500; + return error instanceof RefreshTokenError && error.status >= 400 && error.status < 500; } const extractErrorMessage = async (response: Response): Promise => { From 37b7d11ae1ee4d26177a31efa6e94161a52b27ef Mon Sep 17 00:00:00 2001 From: Mng <50384638+Mng-dev-ai@users.noreply.github.com> Date: Sun, 22 Feb 2026 03:01:56 +0200 Subject: [PATCH 2/2] Narrow session invalidation to 401 only Only invalidate the session when the refresh endpoint returns 401 (token is genuinely invalid). Other 4xx like 429 (rate limited) or 422 (validation error) are transient and shouldn't log the user out. --- frontend/src/lib/api.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 81d47cb..b3be5bb 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -88,9 +88,9 @@ async function refreshTokenIfNeeded(baseURL: string): Promise { } // Desktop reinstalls can leave stale refresh tokens in local storage while the backend -// identity/session store resets. Treat refresh 4xx as terminal to break retry loops. +// identity/session store resets. Treat refresh 401 as terminal to break retry loops. function shouldInvalidateSession(error: unknown): boolean { - return error instanceof RefreshTokenError && error.status >= 400 && error.status < 500; + return error instanceof RefreshTokenError && error.status === 401; } const extractErrorMessage = async (response: Response): Promise => {