api-wizard
Version:
A powerful TypeScript wrapper for native Fetch API with token management, interceptors, and type-safe HTTP requests
136 lines (135 loc) • 5.01 kB
JavaScript
import { FetchClientImpl } from './fetchClient';
const tokenRefreshManager = {
isRefreshing: false,
waitQueue: [],
};
// 인증 헤더 생성
const createAuthHeaders = (context, token, refreshToken) => {
const { config } = context;
return config.formatAuthHeader
? config.formatAuthHeader(token, refreshToken)
: {
Authorization: `Bearer ${token}`,
...(refreshToken && { refresh: refreshToken }),
};
};
// 토큰 갱신 시도 여부 판단
const shouldAttemptRefresh = (context, error) => {
const { config } = context;
return ((error.response?.status === 401 || error.status === 401) &&
!error._retry &&
!!config.getRefreshToken &&
!!config.getToken &&
!!config.refreshEndpoint);
};
// 토큰 갱신 수행
const performTokenRefresh = async (context) => {
const { fetchClient, config } = context;
const refreshToken = config.getRefreshToken?.();
const token = config.getToken?.();
if (!refreshToken || !token || !config.refreshEndpoint) {
throw new Error('Missing refresh configuration');
}
const authHeaders = createAuthHeaders(context, token, refreshToken);
const response = await fetchClient.post(config.refreshEndpoint, {}, { headers: authHeaders });
return response.data;
};
// 토큰 갱신 처리
const handleTokenRefresh = async (context, error, originalRequest) => {
if (!shouldAttemptRefresh(context, error)) {
return Promise.reject(error);
}
const { config } = context;
error._retry = true;
if (tokenRefreshManager.isRefreshing) {
return new Promise((resolve, reject) => {
tokenRefreshManager.waitQueue.push({ resolve, reject });
});
}
tokenRefreshManager.isRefreshing = true;
try {
const { accessToken, refreshToken } = await performTokenRefresh(context);
config.setToken?.(accessToken);
config.setRefreshToken?.(refreshToken);
// 대기 중인 요청들 처리
tokenRefreshManager.waitQueue.forEach(({ resolve }) => {
resolve(originalRequest());
});
return originalRequest();
}
catch (refreshError) {
config.removeToken?.();
config.removeRefreshToken?.();
config.onTokenExpired?.();
tokenRefreshManager.waitQueue.forEach(({ reject }) => {
reject(refreshError);
});
return Promise.reject(refreshError);
}
finally {
tokenRefreshManager.isRefreshing = false;
tokenRefreshManager.waitQueue = [];
}
};
// FetchClient에 인터셉터 기능을 추가하는 클래스
export class InterceptedFetchClient extends FetchClientImpl {
constructor(config) {
super(config);
this.interceptor = config?.interceptor;
}
// 요청 인터셉터 처리
async handleRequestInterceptor(config) {
let processedConfig = { ...config };
// 사용자 정의 요청 인터셉터
if (this.interceptor?.onRequest) {
processedConfig = this.interceptor.onRequest(processedConfig);
}
// 토큰 자동 추가
if (this.interceptor?.tokenConfig?.getToken) {
const token = this.interceptor.tokenConfig.getToken();
if (token) {
const authHeaders = createAuthHeaders({ fetchClient: this, config: this.interceptor.tokenConfig }, token);
processedConfig.headers = {
...processedConfig.headers,
...authHeaders
};
}
}
return processedConfig;
}
// 응답 인터셉터 처리
async handleResponseInterceptor(response) {
if (this.interceptor?.onResponse) {
return this.interceptor.onResponse(response);
}
return response;
}
// 에러 인터셉터 처리
async handleErrorInterceptor(error, originalRequest) {
// 사용자 정의 에러 인터셉터
if (this.interceptor?.onError) {
return this.interceptor.onError(error);
}
// 토큰 갱신 처리
if (this.interceptor?.tokenConfig) {
return handleTokenRefresh({ fetchClient: this, config: this.interceptor.tokenConfig }, error, originalRequest);
}
return Promise.reject(error);
}
// request 메서드 오버라이드
async request(config) {
try {
// 요청 인터셉터 적용
const processedConfig = await this.handleRequestInterceptor(config);
// 원본 요청 실행
const originalRequest = () => super.request(processedConfig);
const response = await originalRequest();
// 응답 인터셉터 적용
return await this.handleResponseInterceptor(response);
}
catch (error) {
// 에러 인터셉터 적용
return this.handleErrorInterceptor(error, () => super.request(config));
}
}
}