From befba57374a63a3dab7283512e948bb7281bdf7f Mon Sep 17 00:00:00 2001 From: Ultradesu Date: Wed, 8 Apr 2026 15:41:47 +0100 Subject: [PATCH] fix(node-player): auto-refresh expired JWT tokens on 401 Adds an axios response interceptor that catches 401 errors, fetches a fresh access token from /auth/token, and retries the original request. Concurrent refresh attempts are deduplicated. Co-Authored-By: Claude Opus 4.6 (1M context) --- furumi-node-player/client/src/furumiApi.ts | 32 ++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/furumi-node-player/client/src/furumiApi.ts b/furumi-node-player/client/src/furumiApi.ts index fa492fb..fa447c7 100644 --- a/furumi-node-player/client/src/furumiApi.ts +++ b/furumi-node-player/client/src/furumiApi.ts @@ -16,6 +16,38 @@ export function clearAuthToken() { delete furumiApi.defaults.headers.common['Authorization'] } +async function refreshToken(): Promise { + try { + const res = await fetch('/auth/token', { credentials: 'include' }) + if (!res.ok) return false + const data = await res.json() + if (data.access_token) { + setAuthToken(data.access_token) + return true + } + } catch { /* ignore */ } + return false +} + +let refreshPromise: Promise | null = null + +furumiApi.interceptors.response.use( + (response) => response, + async (error) => { + const original = error.config + if (error.response?.status === 401 && !original._retried) { + original._retried = true + // Deduplicate concurrent refresh attempts + if (!refreshPromise) { + refreshPromise = refreshToken().finally(() => { refreshPromise = null }) + } + const ok = await refreshPromise + if (ok) return furumiApi(original) + } + return Promise.reject(error) + }, +) + export async function getArtists(): Promise { const res = await furumiApi.get('/artists').catch(() => null) return res?.data ?? null