From 7106a89e12a5e2c494ef83481dc522817500ab1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=90=EC=A0=95=ED=98=84?= Date: Sun, 23 Feb 2025 00:04:34 +0900 Subject: [PATCH 1/5] modify retry logic --- package-lock.json | 9 +++++ package.json | 1 + src/services/apiClient.ts | 81 +++++++++++++++++++++++++++++++-------- 3 files changed, 74 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index 00190c5f..4d7ed8bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "cookie": "^1.0.2", "dayjs": "^1.11.13", "event-source-polyfill": "^1.0.31", + "jwt-decode": "^4.0.0", "next": "15.1.2", "nookies": "^2.5.2", "nprogress": "^0.2.0", @@ -4266,6 +4267,14 @@ "node": ">=4.0" } }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "engines": { + "node": ">=18" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", diff --git a/package.json b/package.json index e04a5f2e..6602b29a 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "cookie": "^1.0.2", "dayjs": "^1.11.13", "event-source-polyfill": "^1.0.31", + "jwt-decode": "^4.0.0", "next": "15.1.2", "nookies": "^2.5.2", "nprogress": "^0.2.0", diff --git a/src/services/apiClient.ts b/src/services/apiClient.ts index 1a84d87d..60d25083 100644 --- a/src/services/apiClient.ts +++ b/src/services/apiClient.ts @@ -2,7 +2,7 @@ import axios from "axios"; import { getAccessToken, removeAccessToken } from "@/utils/tokenUtils"; import authService from "./authService"; import router from "next/router"; -import useAuthStore from "@/stores/useAuthStore"; +import { jwtDecode } from "jwt-decode"; const apiClient = axios.create({ baseURL: process.env.NEXT_PUBLIC_API_URL, @@ -11,40 +11,87 @@ const apiClient = axios.create({ }, }); +let isRefreshing = false; +let failedQueue: any[] = []; + +const processQueue = (error: any, token: string | null = null) => { + failedQueue.forEach(prom => { + if (error) { + prom.reject(error); + } else { + prom.resolve(token); + } + }); + failedQueue = []; +}; + // request apiClient.interceptors.request.use( - (config) => { - const accessToken = getAccessToken(); - if (accessToken) { - config.headers["Authorization"] = `Bearer ${accessToken}`; + async (config) => { + const token = getAccessToken(); + + if (!token) { + return config; + } + + const decoded: any = jwtDecode(token); + const expirationTime = decoded.exp * 1000; // Convert to milliseconds + const currentTime = Date.now(); + const timeBuffer = 60 * 1000; // 1 minute buffer + + if (expirationTime - currentTime < timeBuffer) { + if (!isRefreshing) { + isRefreshing = true; + try { + const newToken = await authService.refreshToken(); + isRefreshing = false; + processQueue(null, newToken); + config.headers["Authorization"] = `Bearer ${newToken}`; + } catch (error) { + processQueue(error, null); + removeAccessToken(); + router.push("/login"); + throw error; + } + } else { + // Wait for the token refresh + try { + const newToken = await new Promise((resolve, reject) => { + failedQueue.push({ resolve, reject }); + }); + config.headers["Authorization"] = `Bearer ${newToken}`; + } catch (error) { + throw error; + } + } + } else { + config.headers["Authorization"] = `Bearer ${token}`; } + return config; }, - (error) => { - return Promise.reject(error); - }, + (error) => Promise.reject(error) ); // response apiClient.interceptors.response.use( (response) => response, async (error) => { - if (error.response && error.response?.status === 401) { + // Keep 401 handler as fallback + if (error.response?.status === 401 && !error.config._retry) { + error.config._retry = true; try { - const newAccessToken = await authService.refreshToken(); - alert("새 토큰 발급!"); - error.config.headers["Authorization"] = `Bearer ${newAccessToken}`; + const newToken = await authService.refreshToken(); + error.config.headers["Authorization"] = `Bearer ${newToken}`; return apiClient(error.config); - } catch (error: any) { - console.error("토큰 갱신 실패", error); + } catch (refreshError) { removeAccessToken(); router.push("/login"); - alert("새로 로그인해주세요!"); - return Promise.reject(error); + return Promise.reject(refreshError); } } return Promise.reject(error); - }, + } ); export default apiClient; From 4a3c75224dedf67523351833ebe4b46203941c3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=90=EC=A0=95=ED=98=84?= Date: Sun, 23 Feb 2025 00:26:21 +0900 Subject: [PATCH 2/5] remove duplicated logic and rely on 401 error from server --- src/services/apiClient.ts | 81 ++++++++++++++++----------------------- 1 file changed, 33 insertions(+), 48 deletions(-) diff --git a/src/services/apiClient.ts b/src/services/apiClient.ts index 60d25083..08a78fd4 100644 --- a/src/services/apiClient.ts +++ b/src/services/apiClient.ts @@ -2,7 +2,6 @@ import axios from "axios"; import { getAccessToken, removeAccessToken } from "@/utils/tokenUtils"; import authService from "./authService"; import router from "next/router"; -import { jwtDecode } from "jwt-decode"; const apiClient = axios.create({ baseURL: process.env.NEXT_PUBLIC_API_URL, @@ -12,84 +11,70 @@ const apiClient = axios.create({ }); let isRefreshing = false; -let failedQueue: any[] = []; +let failedQueue: Array<{ + resolve: (token: string) => void; + reject: (error: any) => void; +}> = []; const processQueue = (error: any, token: string | null = null) => { failedQueue.forEach(prom => { if (error) { prom.reject(error); - } else { + } else if (token) { prom.resolve(token); } }); failedQueue = []; }; -// request +// Only handle token refresh in response interceptor apiClient.interceptors.request.use( async (config) => { const token = getAccessToken(); - - if (!token) { - return config; - } - - const decoded: any = jwtDecode(token); - const expirationTime = decoded.exp * 1000; // Convert to milliseconds - const currentTime = Date.now(); - const timeBuffer = 60 * 1000; // 1 minute buffer - - if (expirationTime - currentTime < timeBuffer) { - if (!isRefreshing) { - isRefreshing = true; - try { - const newToken = await authService.refreshToken(); - isRefreshing = false; - processQueue(null, newToken); - config.headers["Authorization"] = `Bearer ${newToken}`; - } catch (error) { - processQueue(error, null); - removeAccessToken(); - router.push("/login"); - throw error; - } - } else { - // Wait for the token refresh - try { - const newToken = await new Promise((resolve, reject) => { - failedQueue.push({ resolve, reject }); - }); - config.headers["Authorization"] = `Bearer ${newToken}`; - } catch (error) { - throw error; - } - } - } else { + if (token) { config.headers["Authorization"] = `Bearer ${token}`; } - return config; }, (error) => Promise.reject(error) ); -// response apiClient.interceptors.response.use( (response) => response, async (error) => { - // Keep 401 handler as fallback - if (error.response?.status === 401 && !error.config._retry) { - error.config._retry = true; + const originalRequest = error.config; + + if (error.response?.status === 401 && !originalRequest._retry) { + if (isRefreshing) { + try { + const token = await new Promise((resolve, reject) => { + failedQueue.push({ resolve, reject }); + }); + originalRequest.headers["Authorization"] = `Bearer ${token}`; + return apiClient(originalRequest); + } catch (err) { + return Promise.reject(err); + } + } + + originalRequest._retry = true; + isRefreshing = true; + try { - const newToken = await authService.refreshToken(); - error.config.headers["Authorization"] = `Bearer ${newToken}`; - return apiClient(error.config); + const token = await authService.refreshToken(); + originalRequest.headers["Authorization"] = `Bearer ${token}`; + processQueue(null, token); + return apiClient(originalRequest); } catch (refreshError) { + processQueue(refreshError, null); removeAccessToken(); router.push("/login"); return Promise.reject(refreshError); + } finally { + isRefreshing = false; } } + return Promise.reject(error); } ); From bd2a84e2dafbfc4f0c24145e213ba96d408f44b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=90=EC=A0=95=ED=98=84?= Date: Sun, 23 Feb 2025 00:33:58 +0900 Subject: [PATCH 3/5] add comments --- src/services/apiClient.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/services/apiClient.ts b/src/services/apiClient.ts index 08a78fd4..42b39e9a 100644 --- a/src/services/apiClient.ts +++ b/src/services/apiClient.ts @@ -27,7 +27,6 @@ const processQueue = (error: any, token: string | null = null) => { failedQueue = []; }; -// Only handle token refresh in response interceptor apiClient.interceptors.request.use( async (config) => { const token = getAccessToken(); @@ -44,11 +43,12 @@ apiClient.interceptors.response.use( async (error) => { const originalRequest = error.config; + // 기존 로직 유지 if (error.response?.status === 401 && !originalRequest._retry) { - if (isRefreshing) { + if (isRefreshing) { // 동시 요청 들어올 시, 중복으로 refreshToken 요청 방지 try { const token = await new Promise((resolve, reject) => { - failedQueue.push({ resolve, reject }); + failedQueue.push({ resolve, reject }); // 이때는 아직 토큰이 재발급 전이니 클라이언트 요청 대기열에 추가 }); originalRequest.headers["Authorization"] = `Bearer ${token}`; return apiClient(originalRequest); @@ -63,12 +63,12 @@ apiClient.interceptors.response.use( try { const token = await authService.refreshToken(); originalRequest.headers["Authorization"] = `Bearer ${token}`; - processQueue(null, token); + processQueue(null, token); // 실질적인 요청 처리 return apiClient(originalRequest); } catch (refreshError) { processQueue(refreshError, null); removeAccessToken(); - router.push("/login"); + router.push("/login"); // 토큰 재발급 실패 했을때 처리 필요함 return Promise.reject(refreshError); } finally { isRefreshing = false; From 0ba1c0f0ac497dddd59eaf51815e8d32c52a5bff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=90=EC=A0=95=ED=98=84?= Date: Sun, 23 Feb 2025 00:34:21 +0900 Subject: [PATCH 4/5] Add comments --- src/services/apiClient.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/services/apiClient.ts b/src/services/apiClient.ts index 42b39e9a..fee8b37c 100644 --- a/src/services/apiClient.ts +++ b/src/services/apiClient.ts @@ -51,7 +51,7 @@ apiClient.interceptors.response.use( failedQueue.push({ resolve, reject }); // 이때는 아직 토큰이 재발급 전이니 클라이언트 요청 대기열에 추가 }); originalRequest.headers["Authorization"] = `Bearer ${token}`; - return apiClient(originalRequest); + return apiClient(originalRequest); // 원래 본 요청 처리 } catch (err) { return Promise.reject(err); } @@ -63,8 +63,8 @@ apiClient.interceptors.response.use( try { const token = await authService.refreshToken(); originalRequest.headers["Authorization"] = `Bearer ${token}`; - processQueue(null, token); // 실질적인 요청 처리 - return apiClient(originalRequest); + processQueue(null, token); // 기존 대기열에 쌓여있던 요청들에게 토큰 전달 + return apiClient(originalRequest); // 원래 본 요청 처리 } catch (refreshError) { processQueue(refreshError, null); removeAccessToken(); From ab58e98e58074500a477985c3fcf56e95f15c4f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=90=EC=A0=95=ED=98=84?= Date: Sun, 23 Feb 2025 00:35:52 +0900 Subject: [PATCH 5/5] remove unused package --- package-lock.json | 9 --------- package.json | 1 - 2 files changed, 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4d7ed8bf..00190c5f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,6 @@ "cookie": "^1.0.2", "dayjs": "^1.11.13", "event-source-polyfill": "^1.0.31", - "jwt-decode": "^4.0.0", "next": "15.1.2", "nookies": "^2.5.2", "nprogress": "^0.2.0", @@ -4267,14 +4266,6 @@ "node": ">=4.0" } }, - "node_modules/jwt-decode": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", - "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", - "engines": { - "node": ">=18" - } - }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", diff --git a/package.json b/package.json index 6602b29a..e04a5f2e 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,6 @@ "cookie": "^1.0.2", "dayjs": "^1.11.13", "event-source-polyfill": "^1.0.31", - "jwt-decode": "^4.0.0", "next": "15.1.2", "nookies": "^2.5.2", "nprogress": "^0.2.0",