From 6057b60b0a656c532a3422e6099ef29c7556f056 Mon Sep 17 00:00:00 2001 From: waterWang <672684719@qq.com> Date: Sat, 22 Aug 2026 19:46:09 +0800 Subject: [PATCH] fix: handle 401 responses globally with axios interceptor Add a global axios response interceptor so expired sessions never surface as silent spinners or empty tables. On 401 (non-auth endpoints) the interceptor: clears the payd_auth_token from localStorage, shows a sonner toast 'Session expired, please log in again', and redirects to /login. Registers on both the shared api instance and bare axios calls, with guards for /auth endpoints and redirect loops. Closes #473 --- frontend/src/main.tsx | 1 + frontend/src/utils/api.ts | 37 ++++++++++++++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index f84f1879..277888c7 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -2,6 +2,7 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App.tsx'; import './index.css'; +import './utils/api'; // registers global axios 401 interceptor import { BrowserRouter } from 'react-router-dom'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { WalletProvider } from './providers/WalletProvider.tsx'; diff --git a/frontend/src/utils/api.ts b/frontend/src/utils/api.ts index e13771e3..a9dcc2ac 100644 --- a/frontend/src/utils/api.ts +++ b/frontend/src/utils/api.ts @@ -1,4 +1,5 @@ -import axios from 'axios'; +import axios, { AxiosError } from 'axios'; +import { toast } from 'sonner'; const api = axios.create({ baseURL: import.meta.env.VITE_API_URL || 'http://localhost:4000/api', @@ -21,6 +22,40 @@ api.interceptors.request.use( } ); +const AUTH_TOKEN_KEY = 'payd_auth_token'; +const LOGIN_PATH = '/login'; + +function handleUnauthorized(error: AxiosError) { + if (error.response?.status !== 401) { + return Promise.reject(error); + } + + // Skip auth endpoints (login itself may legitimately return 401 for bad credentials). + const requestUrl = error.config?.url ?? ''; + if (requestUrl.includes('/auth/')) { + return Promise.reject(error); + } + + const hadToken = Boolean(localStorage.getItem(AUTH_TOKEN_KEY)); + localStorage.removeItem(AUTH_TOKEN_KEY); + + const currentPath = window.location.pathname; + + if (hadToken && currentPath !== LOGIN_PATH) { + toast.error('Session expired, please log in again'); + window.location.assign(LOGIN_PATH); + } + + return Promise.reject(error); +} + +// Register the 401 handler on the shared axios instance. +api.interceptors.response.use((response) => response, handleUnauthorized); + +// Also handle 401 globally for every bare `axios.*` call across the app so an +// expired session never surfaces as a silent spinner/empty table. +axios.interceptors.response.use((response) => response, handleUnauthorized); + export default api; export interface ApiError extends Error {