Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 18 additions & 9 deletions src/api/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import axios from 'axios';

import { CommonErrorResponse, CommonSuccessResponse } from '@/types/service/common';

import { API } from '..';

export const baseAPI = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_BASE_URL,
timeout: 20000,
Expand Down Expand Up @@ -46,17 +48,24 @@ baseAPI.interceptors.response.use(

const status = error.response?.status ?? errorResponse.status;
const isServer = typeof window === 'undefined';
const originalRequest = error.config;

if (status === 401) {
if (isServer) {
const { redirect } = await import('next/navigation');
redirect('/login');
} else {
if (window.location.pathname === '/login') {
throw errorResponse;
if (status === 401 && !originalRequest._retry) {
try {
await API.authService.refresh();
return baseAPI(originalRequest);
} catch (refreshError) {
if (isServer) {
const { redirect } = await import('next/navigation');
redirect('/login');
} else {
if (window.location.pathname === '/login') {
throw errorResponse;
}
const currentPath = window.location.pathname + window.location.search;
window.location.href = `/login?error=unauthorized&path=${encodeURIComponent(currentPath)}`;
}
const currentPath = window.location.pathname + window.location.search;
window.location.href = `/login?error=unauthorized&path=${encodeURIComponent(currentPath)}`;
throw refreshError;
}
}
Comment on lines 53 to 71
Copy link
Contributor

@coderabbitai coderabbitai bot Dec 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

_retry 플래그 설정이 누락되어 무한 루프가 발생할 수 있습니다.

originalRequest._retry를 확인하지만, 재시도 전에 true로 설정하지 않습니다. 재시도된 요청이 다시 401을 반환하면 또 다른 refresh 시도가 발생하여 무한 루프에 빠질 수 있습니다.

🔎 수정 제안
    if (status === 401 && !originalRequest._retry) {
+     originalRequest._retry = true;
      try {
        await API.authService.refresh();
        return baseAPI(originalRequest);
      } catch (refreshError) {
🤖 Prompt for AI Agents
In src/api/core/index.ts around lines 53 to 70, the code checks
originalRequest._retry but never sets it, which can cause repeated refresh
attempts and an infinite loop; set originalRequest._retry = true before calling
API.authService.refresh() (so the retried request won’t trigger another
refresh), then proceed with the refresh and re-dispatch of the request; ensure
the flag is set on the same request object passed to baseAPI and do not rely on
external state.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이미 API.authService.refresh 함수에서 _retry: true로 강제 설정하고 있기 때문에 무한 루프가 발생하지 않습니다.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Get the auth-service refresh function implementation
cat src/api/service/auth-service/index.ts

Length of output: 1053


@Chiman2937, 말씀하신 내용을 확인했습니다. 다만 약간의 혼동이 있는 것 같습니다:

현재 구현:

  • API.authService.refresh()에서 { _retry: true }를 설정하는 것은 refresh 요청 자체 (POST /auth/refresh)에 대한 플래그입니다
  • 이것은 refresh 엔드포인트가 401을 반환할 때 재시도를 방지합니다

하지만:

  • originalRequest(처음 401을 받은 요청)에는 _retry 플래그가 여전히 설정되지 않습니다
  • 만약 refresh가 성공했지만 토큰이 여전히 유효하지 않거나, 경쟁 조건이 발생하면:
    1. return baseAPI(originalRequest) 재시도
    2. 다시 401 응답 받음
    3. !originalRequest._retry 조건이 여전히 true (undefined)
    4. 또 다른 refresh 시도 → 잠재적 무한 루프

권장 사항:

if (status === 401 && !originalRequest._retry) {
  originalRequest._retry = true;  // 이 라인 추가
  try {
    await API.authService.refresh();
    return baseAPI(originalRequest);

이렇게 하면 모든 엣지 케이스를 안전하게 방지할 수 있습니다. 현재 로직이 대부분의 경우 동작하더라도, originalRequest._retry를 명시적으로 설정하는 것이 더 안전한 방법입니다.


🧬 Code Graph Analysis Results

[src/api/core/index.ts] 401 handling block (shows use of originalRequest._retry and refresh flow)

if (status === 401 && !originalRequest._retry) {
try {
await API.authService.refresh();
return baseAPI(originalRequest);
} catch (refreshError) {
if (isServer) {
const { redirect } = await import('next/navigation');
redirect('/login');
} else {
if (window.location.pathname === '/login') {
throw errorResponse;
}
const currentPath = window.location.pathname + window.location.search;
window.location.href = /login?error=unauthorized&path=${encodeURIComponent(currentPath)};
}
throw refreshError;
}
}

[src/api/index.ts] provide API service reference (relevant to understanding how API.authService.refresh is accessed)

export const API = provideAPIService();

[src/types/service/common.ts] Common error/success response types (context for error handling in interceptors)

export interface CommonErrorResponse {
type: string;
title: string;
status: number;
detail: string;
instance: string;
errorCode?: string;
}

export interface CommonSuccessResponse {
status: number;
success: boolean;
data: T;
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 내용은 논리적으로 이해 되지 않습니다.

  1. GET /api/v1/users/mePOST /api/v1/groups/create api가 동시에 호출된다고 가정하겠습니다.
    이때 originalRequest._retry를 true로 설정한다고 해도 각 api 호출 시 axios 인스턴스가 별도로 생성되기 때문에 _retry 플래그를 true로 설정한다고 해서 서로 공유되지 않습니다. 이 때문에 _retry 플래그를 true를 설정하는 것이 의미가 없다고 생각됩니다.

  2. 애초에 status 401이 발생했을 때는 refresh 호출 로직밖에 없고, 그 후에는 다시 원래 api를 호출하는 것이 아닌 /login 페이지로 리다이렉트 되면서 api 호출을 중단합니다.

여기서 무한루프를 일으킬 여지가 있나요?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Chiman2937, 좋은 질문입니다. 제가 설명을 더 명확히 드리겠습니다.

1번 포인트에 대한 답변: _retry 플래그의 위치

_retry 플래그는 axios 인스턴스가 아닌 요청 config 객체(error.config)에 설정됩니다.

const originalRequest = error.config;  // 이것이 요청별 config 객체입니다

동시에 여러 API가 호출되는 경우:

  • GET /api/v1/users/me의 config 객체 → 각자의 _retry 플래그 보유
  • POST /api/v1/groups/create의 config 객체 → 각자의 _retry 플래그 보유

각 요청은 자체 config 객체를 가지므로, 서로 간섭하지 않습니다. 따라서 originalRequest._retry = true 설정은 그 특정 요청에 대한 재시도만 방지합니다.

2번 포인트에 대한 답변: 무한 루프 시나리오

코드를 다시 살펴보면:

if (status === 401 && !originalRequest._retry) {
  try {
    await API.authService.refresh();  // refresh 성공
    return baseAPI(originalRequest);   // 원래 요청 재시도 ← 여기가 핵심
  } catch (refreshError) {
    // refresh 실패 시에만 /login으로 리다이렉트
  }
}

무한 루프 발생 시나리오:

  1. 요청 A가 401 반환
  2. !originalRequest._retry 체크 통과 (undefined)
  3. API.authService.refresh() 호출 → 성공
  4. return baseAPI(originalRequest) - 요청 A 재시도
  5. 만약 재시도된 요청 A가 다시 401을 반환하면? (예: 경쟁 조건, 네트워크 지연, 서버 문제 등)
  6. 다시 인터셉터 진입 → !originalRequest._retry 체크 (여전히 undefined) → 통과
  7. 다시 refresh 시도 → 2번으로 돌아가며 무한 루프

중요한 차이점:

  • API.authService.refresh(){ _retry: true }refresh 엔드포인트 자체가 401을 받을 때만 보호
  • originalRequest._retry = true원래 실패한 요청(예: users/me)이 재시도 후 다시 401을 받을 때 보호

이것이 표준적인 axios 인터셉터 refresh 패턴입니다. 대부분의 경우 문제없이 작동하지만, 엣지 케이스를 방지하기 위해 originalRequest._retry = true를 설정하는 것이 권장됩니다.

혹시 제 설명에서 여전히 이해가 안 되는 부분이 있으시면 말씀해 주세요! 🙂

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refresh api에서 정상적으로 200응답이 온 직후더라도 다시 401 에러가 발생하는 경우를 대비한 설정이라는 건가요?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Chiman2937, 네, 정확히 이해하셨습니다! 👍

시나리오:

  1. Refresh API가 200으로 성공 응답
  2. 새로운 accessToken이 쿠키에 설정됨
  3. return baseAPI(originalRequest) 실행 - 원래 요청 재시도
  4. 그런데 이 재시도된 요청이 다시 401을 반환하는 경우

이런 경우가 발생할 수 있는 상황:

  • 경쟁 조건(Race Condition): 여러 요청이 동시에 401을 받고 refresh를 시도할 때
  • 토큰 전파 지연: 쿠키가 설정되었지만 다음 요청에서 아직 반영되지 않은 경우
  • 서버 측 타이밍 이슈: 새 토큰이 아직 서버에서 완전히 활성화되지 않은 경우
  • 네트워크 지연: 요청 헤더가 이전 상태를 참조하는 경우

결론:
originalRequest._retry = true 설정은 이런 엣지 케이스를 방어하기 위한 안전장치입니다. 대부분의 경우 문제가 없지만, 예상치 못한 401 재발생 시 무한 루프를 확실하게 방지합니다.

이것이 일반적인 axios 인터셉터 refresh 패턴에서 권장되는 이유입니다. 하지만 최종 결정은 팀의 판단에 맡기겠습니다! 😊

if (status === 404) {
Expand Down
6 changes: 5 additions & 1 deletion src/api/service/auth-service/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ export const authServiceRemote = () => ({

// 액세스 토큰 재발급
refresh: async () => {
const data = await api.post<RefreshResponse>('/auth/refresh');
const data = await api.post<RefreshResponse>(
'/auth/refresh',
{},
{ _retry: true, withCredentials: true },
);

setAccessToken(data.accessToken, data.expiresIn);
return data;
Expand Down