Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: default content-type #30

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
31 changes: 21 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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<Response> | - |
| 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<Response> | - |
| 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`
Copy link
Member

Choose a reason for hiding this comment

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

data가 json인 경우에는 저희가 application/json으로 "설정"해주는 것이기에
is 대신 is set to 요론식으로 작성하면 어떨까요 ~?

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
37 changes: 26 additions & 11 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export type FetchAXDefaultOptions = {
};
const parseResponseData = async <T>(
response: Response,
type: ResponseType,
type?: ResponseType,
): Promise<T> => {
switch (type) {
case 'arraybuffer':
Expand Down Expand Up @@ -204,6 +204,15 @@ export interface RequestInit extends Omit<globalThis.RequestInit, 'body'> {
/** 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 &&
Expand All @@ -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) ||
Copy link
Member

@gahyuun gahyuun Jan 12, 2025

Choose a reason for hiding this comment

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

이 코드는 어떤 의도로 추가된걸까요 !? 혹시 어떤 오류를 발견하셨나용?

data instanceof Blob ||
data instanceof ArrayBuffer ||
data instanceof FormData ||
Expand Down Expand Up @@ -288,6 +290,7 @@ const applyDefaultOptionsArgs = (
);

const requestHeaders: Record<string, string> = {};

if (defaultOptions?.headers) {
new Headers(defaultOptions.headers).forEach((value, key) => {
requestHeaders[key] = value;
Expand All @@ -299,6 +302,10 @@ const applyDefaultOptionsArgs = (
});
}

if (requestInit?.data) {
appendJsonContentType(requestInit.data, requestHeaders);
}

let requestArgs = {
...defaultOptions,
...requestInit,
Expand Down Expand Up @@ -334,6 +341,16 @@ function isHttpError(response: Response) {
return response.status >= 300;
}

function appendJsonContentType(
Copy link
Member

Choose a reason for hiding this comment

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

저희 rollup 사용한 이후로 파일 분리를 안했네요!! 이 부분 제가 이슈에 추가하겠습니다

data: Record<string, any> | BodyInit,
requestHeaders: Record<string, string>,
) {
if (isJson(JSON.stringify(data)) && !requestHeaders['content-type']) {
requestHeaders['content-type'] = 'application/json';
}
return requestHeaders;
}

function ensureBodyInit(data: BodyInit | Record<string, any>): BodyInit {
if (isBodyInit(data)) {
return data;
Expand Down Expand Up @@ -565,8 +582,6 @@ const fetchAX = {
};
export default fetchAX;
export const presetOptions: FetchAXDefaultOptions = {
headers: { 'Content-Type': 'application/json' },

throwError: true,

// baseURL: ''
Expand Down
29 changes: 25 additions & 4 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down Expand Up @@ -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: {
Expand All @@ -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', () => {
Expand Down