-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* rename(packages/utils): utils 패키지로 fetcher 테스트 파일 이동 * feat(packages/utils): lodash 관련 라이브러리 설치 * feat(packages/utils): fetcher 클래스 타입 정의 및 속성, 생성자 정의 * refactor(packages/utils): fetcher 클래스 baseUrl, header 관리 방법 수정 * refactor(packages/utils): fetcher 클래스 응답, 요청 인터셉터 구현체 구현 * chore(packages/utils): 요청 인터셉터에서 기존 헤더 값 고려하도록 수정 * feat(packages/utils): fetcher 클래스 request 함수 정의 및 전역 에러 핸들러 추가 * chore(packages/utils): fetcher 클래스 인터셉터 생성자 인수 받을 수 있도록 수정 * feat(packages/utils): HTTP 메서드 정의 및 에러 핸들러 함수 변경 * refactor(packages/utils): 에러 핸들링 부분 HTTPError 객체로 리팩토링
- Loading branch information
1 parent
f8f8de7
commit 342cda7
Showing
5 changed files
with
360 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
class HTTPError { | ||
static handle(response: Response) { | ||
const error = new Error(); | ||
|
||
switch (response.status) { | ||
case 400: | ||
error.name = "Bad Request"; | ||
error.message = "The request was invalid or cannot be served."; | ||
break; | ||
case 401: | ||
error.name = "Unauthorized"; | ||
error.message = | ||
"Authentication is required and has failed or has not yet been provided."; | ||
break; | ||
case 403: | ||
error.name = "Forbidden"; | ||
error.message = | ||
"The request was valid, but the server is refusing action."; | ||
break; | ||
case 404: | ||
error.name = "Not Found"; | ||
error.message = "The requested resource could not be found."; | ||
break; | ||
case 500: | ||
error.name = "Internal Server Error"; | ||
error.message = "An error occurred on the server."; | ||
break; | ||
case 502: | ||
error.name = "Bad Gateway"; | ||
error.message = | ||
"The server received an invalid response from the upstream server."; | ||
break; | ||
case 503: | ||
error.name = "Service Unavailable"; | ||
error.message = "The server is currently unable to handle the request."; | ||
break; | ||
default: | ||
error.name = "HTTP Error"; | ||
error.message = `An unexpected HTTP error occurred: ${response.status}`; | ||
break; | ||
} | ||
|
||
return error; | ||
} | ||
} | ||
|
||
export default HTTPError; |
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,298 @@ | ||
import { isEqual } from "lodash"; | ||
import HTTPError from "../errorHandler/HTTPError.ts"; | ||
|
||
type ApiResponse<T> = Response & { data?: T; success?: boolean }; | ||
|
||
type RequestInterceptor = ( | ||
options: RequestInit | ||
) => RequestInit | Promise<RequestInit>; | ||
|
||
type ResponseInterceptor<T = any> = ( | ||
response: Response | ||
) => ApiResponse<T> | Promise<ApiResponse<T>>; | ||
|
||
interface BaseUrlConfig { | ||
value: string; | ||
set: (url: string) => void; | ||
} | ||
|
||
interface HeadersConfig { | ||
value: HeadersInit; | ||
set: (headers: HeadersInit) => void; | ||
merge: (headers: HeadersInit) => void; | ||
} | ||
|
||
interface Interceptors { | ||
request: { | ||
handlers: RequestInterceptor[]; | ||
add: (interceptor: RequestInterceptor) => void; | ||
remove: (interceptor: RequestInterceptor) => void; | ||
}; | ||
response: { | ||
handlers: ResponseInterceptor[]; | ||
add: (interceptor: ResponseInterceptor) => void; | ||
remove: (interceptor: ResponseInterceptor) => void; | ||
}; | ||
} | ||
|
||
interface ErrorHandler { | ||
handler: (error: Error) => void | Promise<void>; | ||
set: (handler: (error: Error) => void | Promise<void>) => void; | ||
} | ||
|
||
class Fetcher { | ||
private baseUrlConfig: BaseUrlConfig; | ||
private headersConfig: HeadersConfig; | ||
private interceptors: Interceptors; | ||
private errorHandler: ErrorHandler; | ||
|
||
constructor({ | ||
baseUrl = "", | ||
defaultHeaders = {}, | ||
requestInterceptors = [], | ||
responseInterceptors = [], | ||
errorHandler = () => {}, | ||
} = {}) { | ||
this.baseUrlConfig = this.createBaseUrlConfig(baseUrl); | ||
this.headersConfig = this.createHeadersConfig(defaultHeaders); | ||
this.interceptors = this.createInterceptors( | ||
requestInterceptors, | ||
responseInterceptors | ||
); | ||
this.errorHandler = this.createErrorHandler(errorHandler); | ||
} | ||
|
||
private createBaseUrlConfig(baseUrl: string): BaseUrlConfig { | ||
return { | ||
value: baseUrl, | ||
set: (url: string) => { | ||
this.baseUrlConfig.value = url; | ||
}, | ||
}; | ||
} | ||
|
||
private createHeadersConfig(defaultHeaders: HeadersInit): HeadersConfig { | ||
return { | ||
value: defaultHeaders, | ||
set: (headers: HeadersInit) => { | ||
this.headersConfig.value = headers; | ||
}, | ||
merge: (headers: HeadersInit) => { | ||
this.headersConfig.value = { | ||
...this.headersConfig.value, | ||
...headers, | ||
}; | ||
}, | ||
}; | ||
} | ||
|
||
private createInterceptors( | ||
requestInterceptors: RequestInterceptor[], | ||
responseInterceptors: ResponseInterceptor[] | ||
): Interceptors { | ||
return { | ||
request: { | ||
handlers: requestInterceptors, | ||
add: (interceptor: RequestInterceptor) => { | ||
if (!this.interceptors.request.handlers.includes(interceptor)) { | ||
this.interceptors.request.handlers.push(interceptor); | ||
} | ||
}, | ||
remove: (interceptor: RequestInterceptor) => { | ||
this.interceptors.request.handlers = | ||
this.interceptors.request.handlers.filter( | ||
(currentInterceptor) => | ||
!isEqual(currentInterceptor.toString(), interceptor.toString()) | ||
); | ||
}, | ||
}, | ||
response: { | ||
handlers: responseInterceptors, | ||
add: (interceptor: ResponseInterceptor) => { | ||
if (!this.interceptors.response.handlers.includes(interceptor)) { | ||
this.interceptors.response.handlers.push(interceptor); | ||
} | ||
}, | ||
remove: (interceptor: ResponseInterceptor) => { | ||
this.interceptors.response.handlers = | ||
this.interceptors.response.handlers.filter( | ||
(currentInterceptor) => | ||
!isEqual(currentInterceptor.toString(), interceptor.toString()) | ||
); | ||
}, | ||
}, | ||
}; | ||
} | ||
|
||
private createErrorHandler( | ||
errorHandler: (error: Error) => void | Promise<void> | ||
): ErrorHandler { | ||
return { | ||
handler: errorHandler, | ||
set: (handler: (error: Error) => void | Promise<void>) => { | ||
this.errorHandler.handler = handler; | ||
}, | ||
}; | ||
} | ||
|
||
public addRequestInterceptor(interceptor: RequestInterceptor): void { | ||
this.interceptors.request.add(interceptor); | ||
} | ||
|
||
public removeRequestInterceptors(interceptor: RequestInterceptor): void { | ||
this.interceptors.request.remove(interceptor); | ||
} | ||
|
||
public async interceptRequests(options: RequestInit): Promise<RequestInit> { | ||
let interceptedOptions: RequestInit = { | ||
...options, | ||
headers: { | ||
...(this.headersConfig.value || {}), | ||
...(options.headers || {}), | ||
}, | ||
}; | ||
for (const interceptor of this.interceptors.request.handlers) { | ||
interceptedOptions = await interceptor(interceptedOptions); | ||
} | ||
return interceptedOptions; | ||
} | ||
|
||
public addResponseInterceptor(interceptor: ResponseInterceptor): void { | ||
this.interceptors.response.add(interceptor); | ||
} | ||
|
||
public removeResponseInterceptors(interceptor: ResponseInterceptor): void { | ||
this.interceptors.response.remove(interceptor); | ||
} | ||
|
||
public async interceptResponses<T>( | ||
response: Response | ||
): Promise<ApiResponse<T>> { | ||
let interceptedResponse = response; | ||
for (const interceptor of this.interceptors.response.handlers) { | ||
interceptedResponse = await interceptor(interceptedResponse); | ||
} | ||
return interceptedResponse; | ||
} | ||
|
||
public async request<T>( | ||
url: string, | ||
options: RequestInit | ||
): Promise<ApiResponse<T>> { | ||
try { | ||
const interceptedOptions = await this.interceptRequests(options); | ||
const fetchOptions: RequestInit = { | ||
...interceptedOptions, | ||
credentials: "include", | ||
}; | ||
const requestUrl = this.baseUrlConfig.value + url; | ||
|
||
let response: ApiResponse<any> = await fetch(requestUrl, fetchOptions); | ||
|
||
const data = await this.parseResponseData(response); | ||
const error = this.handleError(response); | ||
|
||
if (error) { | ||
throw error; | ||
} | ||
|
||
response = await this.interceptResponses(response); | ||
response.data = data; | ||
|
||
return response; | ||
} catch (error) { | ||
if (error instanceof Error) { | ||
this.errorHandler.handler(error); | ||
} | ||
|
||
throw error; | ||
} | ||
} | ||
|
||
private async parseResponseData<T>( | ||
response: Response | ||
): Promise<T | Blob | string> { | ||
const contentType = response.headers.get("Content-Type") || ""; | ||
|
||
if (contentType.includes("application/json")) { | ||
return response.json() as Promise<T>; | ||
} else if ( | ||
contentType.startsWith("image/") || | ||
contentType.startsWith("application/octet-stream") | ||
) { | ||
return response.blob(); | ||
} | ||
|
||
return response.text(); | ||
} | ||
|
||
private handleError(response: Response): Error | undefined { | ||
if (!response.ok) { | ||
return HTTPError.handle(response); | ||
} | ||
} | ||
|
||
public get<T>( | ||
url: string, | ||
options: RequestInit = {}, | ||
params: Record<string, any> = {} | ||
): Promise<ApiResponse<T>> { | ||
const queryString = | ||
params && Object.keys(params).length | ||
? `?${new URLSearchParams(params).toString()}` | ||
: ""; | ||
const fullUrl = `${url}${queryString}`; | ||
|
||
return this.request(fullUrl, { ...options, method: "GET" }); | ||
} | ||
|
||
public post<T>( | ||
url: string, | ||
body = {}, | ||
options: RequestInit = {} | ||
): Promise<ApiResponse<T>> { | ||
return this.request(url, { | ||
...options, | ||
method: "POST", | ||
body: JSON.stringify(body), | ||
}); | ||
} | ||
|
||
public put<T>( | ||
url: string, | ||
body = {}, | ||
options: RequestInit = {} | ||
): Promise<ApiResponse<T>> { | ||
return this.request(url, { | ||
...options, | ||
method: "PUT", | ||
body: JSON.stringify(body), | ||
}); | ||
} | ||
|
||
public patch<T>( | ||
url: string, | ||
body = {}, | ||
options: RequestInit = {} | ||
): Promise<ApiResponse<T>> { | ||
return this.request(url, { | ||
...options, | ||
method: "PATCH", | ||
body: JSON.stringify(body), | ||
}); | ||
} | ||
|
||
public delete<T>( | ||
url: string, | ||
body = {}, | ||
options: RequestInit = {} | ||
): Promise<ApiResponse<T>> { | ||
return this.request(url, { | ||
...options, | ||
method: "DELETE", | ||
body: JSON.stringify(body), | ||
}); | ||
} | ||
} | ||
|
||
export default Fetcher; |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.