diff --git a/README.md b/README.md index 1beaf5a..164e4e8 100644 --- a/README.md +++ b/README.md @@ -166,7 +166,8 @@ The available instance methods are listed below. ## ✔️ Parsing Response -If you set a response type, you can parse the response with that type. The default type is 'json'. +If you set a response type, you can parse the response with that type. +If responseType is not set, the original data is returned. But parsed as `json` only if the content-type is `application/json`. ```tsx const instance = fetchAX.create(); @@ -265,12 +266,22 @@ const instance = fetchAX.create({ ### default options -| Property | Description | Type | Default | -| --------------------------- | ---------------------------------------- | ---------------------------------------------------------------------------------- | --------------------------------------------------- | -| baseURL | base url | string \| URL | - | -| headers | fetch headers | HeadersInit | new Headers([['Content-Type', 'application/json']]) | -| throwError | whether to throw an error | boolean | true | -| responseType | response type to parse | ResponseType | - | -| responseInterceptor | interceptor to be executed on response | (response: Response) => Response \| Promise | - | -| responseRejectedInterceptor | interceptor to handle rejected responses | (error: any) => any | - | -| requestInterceptor | interceptor to be executed on request | (requestArg: RequestInitReturnedByInterceptor) => RequestInitReturnedByInterceptor | - | +| Property | Description | Type | Default | +| --------------------------- | ---------------------------------------- | ---------------------------------------------------------------------------------- | ------- | +| baseURL | base url | string \| URL | - | +| headers | fetch headers | HeadersInit | - | +| throwError | whether to throw an error | boolean | true | +| responseType | response type to parse | ResponseType | - | +| responseInterceptor | interceptor to be executed on response | (response: Response) => Response \| Promise | - | +| responseRejectedInterceptor | interceptor to handle rejected responses | (error: any) => any | - | +| requestInterceptor | interceptor to be executed on request | (requestArg: RequestInitReturnedByInterceptor) => RequestInitReturnedByInterceptor | - | + +#### conditional auto default options + +##### responseType + +- By default, there is no default value for responseType. But it is `json` only if the content-type is `application/json` + +##### headers + +- By default, there is no default value for headers. But it's content-type is `application/json` only if the data is `json` diff --git a/package.json b/package.json index 413a4c0..0d765bc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fetch-ax", - "version": "2.0.3", + "version": "2.0.4", "description": "A modern HTTP client that extends the Fetch API, providing Axios-like syntax and full compatibility with Next.js App Router.", "scripts": { "test": "jest", diff --git a/src/index.ts b/src/index.ts index 21c15d7..a7bc60b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -109,7 +109,7 @@ export type FetchAXDefaultOptions = { }; const parseResponseData = async ( response: Response, - type: ResponseType, + type?: ResponseType, ): Promise => { switch (type) { case 'arraybuffer': @@ -204,6 +204,15 @@ export interface RequestInit extends Omit { /** Resposne data's type */ responseType?: ResponseType; } + +const isJson = (data: any) => { + try { + return typeof JSON.parse(data) === 'object'; + } catch (e) { + return false; + } +}; + const isArrayBufferView = (data: any): data is ArrayBufferView => { return ( data && @@ -218,17 +227,10 @@ const isArrayBufferView = (data: any): data is ArrayBufferView => { }; const isBodyInit = (data: any): data is BodyInit => { - const isJson = (data: any) => { - try { - return typeof JSON.parse(data) === 'object'; - } catch (e) { - return false; - } - }; return ( isJson(data) || // data === 'string' 을 통해서도 JSON인지를 확인할 수 있지만 명시적으로 따지기 위해서 typeof data === 'string' || - data instanceof ReadableStream || + (typeof ReadableStream !== 'undefined' && data instanceof ReadableStream) || data instanceof Blob || data instanceof ArrayBuffer || data instanceof FormData || @@ -288,6 +290,7 @@ const applyDefaultOptionsArgs = ( ); const requestHeaders: Record = {}; + if (defaultOptions?.headers) { new Headers(defaultOptions.headers).forEach((value, key) => { requestHeaders[key] = value; @@ -299,6 +302,10 @@ const applyDefaultOptionsArgs = ( }); } + if (requestInit?.data) { + appendJsonContentType(requestInit.data, requestHeaders); + } + let requestArgs = { ...defaultOptions, ...requestInit, @@ -334,6 +341,16 @@ function isHttpError(response: Response) { return response.status >= 300; } +function appendJsonContentType( + data: Record | BodyInit, + requestHeaders: Record, +) { + if (isJson(JSON.stringify(data)) && !requestHeaders['content-type']) { + requestHeaders['content-type'] = 'application/json'; + } + return requestHeaders; +} + function ensureBodyInit(data: BodyInit | Record): BodyInit { if (isBodyInit(data)) { return data; @@ -565,8 +582,6 @@ const fetchAX = { }; export default fetchAX; export const presetOptions: FetchAXDefaultOptions = { - headers: { 'Content-Type': 'application/json' }, - throwError: true, // baseURL: '' diff --git a/test/index.test.ts b/test/index.test.ts index 1588213..d8f9bd9 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -60,9 +60,7 @@ describe('next-fetch', () => { 'https://jsonplaceholder.typicode.com/todos/1', //default { - headers: { - 'content-type': 'application/json', - }, // options들은 headers 객체로 한 번 생성되기 때문에 소문자로 변경됨 + headers: {}, // options들은 headers 객체로 한 번 생성되기 때문에 소문자로 변경됨 method: 'GET', throwError: true, }, @@ -191,7 +189,7 @@ describe('next-fetch', () => { expect(fetchMocked).toHaveBeenCalledWith( 'https://jsonplaceholder.typicode.com/todos/1?id=1', { - headers: { 'content-type': 'application/json' }, + headers: {}, method: 'GET', throwError: true, params: { @@ -200,6 +198,29 @@ describe('next-fetch', () => { }, ); }); + + it('should append content-type application/json if data is a JSON object', async () => { + // given + const instance = fetchAX.create(); + + // when + await instance.post('https://jsonplaceholder.typicode.com/todos/1', { + test: 'test', + }); + + // then + expect(fetchMocked).toHaveBeenCalledWith( + 'https://jsonplaceholder.typicode.com/todos/1', + + { + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ test: 'test' }), + data: { test: 'test' }, + method: 'POST', + throwError: true, + }, + ); + }); }); describe('next-fetch-error', () => {