From 6780700d40e2d66d260b50bfcbb60d9b6d4db702 Mon Sep 17 00:00:00 2001 From: Red Date: Mon, 28 Oct 2024 04:19:59 +0700 Subject: [PATCH 1/5] feat: Introduced Content-type handling in DELETE and PUT methods --- src/constants.ts | 1 + src/request-handler.ts | 38 ++++++++++++++++++-- test/request-handler.spec.ts | 68 ++++++++++++++++++++++++++++++++++-- 3 files changed, 102 insertions(+), 5 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index 59c1853..bd4d045 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,6 +1,7 @@ export const APPLICATION_CONTENT_TYPE = 'application/'; export const APPLICATION_JSON = APPLICATION_CONTENT_TYPE + 'json'; +export const CHARSET_UTF_8 = 'charset=utf-8'; export const CONTENT_TYPE = 'Content-Type'; export const UNDEFINED = 'undefined'; diff --git a/src/request-handler.ts b/src/request-handler.ts index d9ada6b..8dfa476 100644 --- a/src/request-handler.ts +++ b/src/request-handler.ts @@ -12,6 +12,7 @@ import type { FetcherConfig, FetcherInstance, Logger, + HeadersObject, } from './types/request-handler'; import type { BodyPayload, @@ -37,6 +38,7 @@ import { ABORT_ERROR, APPLICATION_JSON, CANCELLED_ERROR, + CHARSET_UTF_8, CONTENT_TYPE, GET, HEAD, @@ -56,7 +58,6 @@ const defaultConfig: RequestHandlerConfig = { headers: { Accept: APPLICATION_JSON + ', text/plain, */*', 'Accept-Encoding': 'gzip, deflate, br', - [CONTENT_TYPE]: APPLICATION_JSON + ';charset=utf-8', }, retry: { delay: 1000, @@ -165,6 +166,35 @@ export function createRequestHandler( } }; + /** + * Sets the Content-Type header to 'application/json;charset=utf-8' if needed based on the method and body. + * + * @param headers - The headers object where Content-Type will be set. + * @param method - The HTTP method (e.g., GET, POST, PUT, DELETE). + * @param body - Optional request body to determine if Content-Type is needed. + */ + const setContentTypeIfNeeded = ( + headers: HeadersInit, + method: string, + body?: unknown, + ): void => { + if (!body && ['PUT', 'DELETE'].includes(method)) return; + + const contentTypeValue = APPLICATION_JSON + ';' + CHARSET_UTF_8; + + if (headers instanceof Headers) { + if (!headers.has(CONTENT_TYPE)) { + headers.set(CONTENT_TYPE, contentTypeValue); + } + } else if ( + typeof headers === 'object' && + !Array.isArray(headers) && + !headers[CONTENT_TYPE] + ) { + headers[CONTENT_TYPE] = contentTypeValue; + } + }; + /** * Build request configuration * @@ -202,6 +232,10 @@ export function createRequestHandler( body = explicitBodyData; } + const headers = getConfig(requestConfig, 'headers'); + + setContentTypeIfNeeded(headers, method, body); + // Native fetch compatible settings const isWithCredentials = getConfig( requestConfig, @@ -236,8 +270,8 @@ export function createRequestHandler( credentials, body, method, - url: baseURL + urlPath, + headers, }; }; diff --git a/test/request-handler.spec.ts b/test/request-handler.spec.ts index b8c2541..df4f2e6 100644 --- a/test/request-handler.spec.ts +++ b/test/request-handler.spec.ts @@ -8,7 +8,7 @@ import type { RequestHandlerReturnType, } from '../src/types/request-handler'; import { fetchf } from '../src'; -import { ABORT_ERROR } from '../src/constants'; +import { ABORT_ERROR, APPLICATION_JSON, CHARSET_UTF_8 } from '../src/constants'; import { ResponseErr } from '../src/response-error'; jest.mock('../src/utils', () => { @@ -26,6 +26,7 @@ const fetcher = { describe('Request Handler', () => { const apiUrl = 'http://example.com/api/'; + const contentTypeValue = APPLICATION_JSON + ';' + CHARSET_UTF_8; const responseMock = { data: { test: 'data', @@ -64,11 +65,13 @@ describe('Request Handler', () => { const headers = { Accept: 'application/json, text/plain, */*', 'Accept-Encoding': 'gzip, deflate, br', - 'Content-Type': 'application/json;charset=utf-8', + 'Content-Type': contentTypeValue, }; beforeAll(() => { - requestHandler = createRequestHandler({}); + requestHandler = createRequestHandler({ + headers, + }); }); it('should not differ when the same request is made', () => { @@ -270,6 +273,65 @@ describe('Request Handler', () => { }); }); + describe('request() Content-Type', () => { + let requestHandler: RequestHandlerReturnType; + const contentTypeValue = 'application/json;charset=utf-8'; + + beforeEach(() => { + requestHandler = createRequestHandler({}); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe.each([ + { method: 'DELETE', body: undefined, expectContentType: false }, + { method: 'PUT', body: undefined, expectContentType: false }, + { method: 'DELETE', body: { foo: 'bar' }, expectContentType: true }, + { method: 'PUT', body: { foo: 'bar' }, expectContentType: true }, + { method: 'POST', body: undefined, expectContentType: true }, + { method: 'GET', body: undefined, expectContentType: true }, + ])( + '$method request with body: $body', + ({ method, body, expectContentType }) => { + it( + expectContentType + ? 'should set Content-Type when body is provided or method requires it' + : 'should not set Content-Type when no body is provided for DELETE or PUT', + () => { + const result = requestHandler.buildConfig(apiUrl, { method, body }); + if (expectContentType) { + expect(result.headers).toHaveProperty( + 'Content-Type', + contentTypeValue, + ); + } else { + expect(result.headers).not.toHaveProperty('Content-Type'); + } + }, + ); + }, + ); + + describe.each(['DELETE', 'PUT'])( + '%s method with custom Content-Type', + (method) => { + it(`should keep custom Content-Type for ${method} method`, () => { + const customContentType = 'application/x-www-form-urlencoded'; + const result = requestHandler.buildConfig(apiUrl, { + method, + headers: { 'Content-Type': customContentType }, + }); + expect(result.headers).toHaveProperty( + 'Content-Type', + customContentType, + ); + }); + }, + ); + }); + describe('request()', () => { beforeEach(() => { jest.useFakeTimers(); From 60ed7c1a318846f3c804a068974ba09906029d59 Mon Sep 17 00:00:00 2001 From: Red Date: Mon, 28 Oct 2024 04:39:24 +0700 Subject: [PATCH 2/5] fix: Removed some strings to have few bytes less --- src/constants.ts | 3 +++ src/request-handler.ts | 10 +++++++--- test/request-handler.spec.ts | 29 +++++++++++++++++++---------- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index bd4d045..dd0a0be 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -13,4 +13,7 @@ export const TIMEOUT_ERROR = 'TimeoutError'; export const CANCELLED_ERROR = 'CanceledError'; export const GET = 'GET'; +export const POST = 'POST'; +export const PUT = 'PUT'; +export const DELETE = 'DELETE'; export const HEAD = 'HEAD'; diff --git a/src/request-handler.ts b/src/request-handler.ts index 8dfa476..4e67b41 100644 --- a/src/request-handler.ts +++ b/src/request-handler.ts @@ -40,9 +40,11 @@ import { CANCELLED_ERROR, CHARSET_UTF_8, CONTENT_TYPE, + DELETE, GET, HEAD, OBJECT, + PUT, STRING, UNDEFINED, } from './constants'; @@ -178,7 +180,9 @@ export function createRequestHandler( method: string, body?: unknown, ): void => { - if (!body && ['PUT', 'DELETE'].includes(method)) return; + if (!body && [PUT, DELETE].includes(method)) { + return; + } const contentTypeValue = APPLICATION_JSON + ';' + CHARSET_UTF_8; @@ -187,7 +191,7 @@ export function createRequestHandler( headers.set(CONTENT_TYPE, contentTypeValue); } } else if ( - typeof headers === 'object' && + typeof headers === OBJECT && !Array.isArray(headers) && !headers[CONTENT_TYPE] ) { @@ -270,8 +274,8 @@ export function createRequestHandler( credentials, body, method, - url: baseURL + urlPath, headers, + url: baseURL + urlPath, }; }; diff --git a/test/request-handler.spec.ts b/test/request-handler.spec.ts index df4f2e6..d1c18c0 100644 --- a/test/request-handler.spec.ts +++ b/test/request-handler.spec.ts @@ -8,7 +8,16 @@ import type { RequestHandlerReturnType, } from '../src/types/request-handler'; import { fetchf } from '../src'; -import { ABORT_ERROR, APPLICATION_JSON, CHARSET_UTF_8 } from '../src/constants'; +import { + ABORT_ERROR, + APPLICATION_JSON, + CHARSET_UTF_8, + CONTENT_TYPE, + DELETE, + GET, + POST, + PUT, +} from '../src/constants'; import { ResponseErr } from '../src/response-error'; jest.mock('../src/utils', () => { @@ -286,12 +295,12 @@ describe('Request Handler', () => { }); describe.each([ - { method: 'DELETE', body: undefined, expectContentType: false }, - { method: 'PUT', body: undefined, expectContentType: false }, - { method: 'DELETE', body: { foo: 'bar' }, expectContentType: true }, - { method: 'PUT', body: { foo: 'bar' }, expectContentType: true }, - { method: 'POST', body: undefined, expectContentType: true }, - { method: 'GET', body: undefined, expectContentType: true }, + { method: DELETE, body: undefined, expectContentType: false }, + { method: PUT, body: undefined, expectContentType: false }, + { method: DELETE, body: { foo: 'bar' }, expectContentType: true }, + { method: PUT, body: { foo: 'bar' }, expectContentType: true }, + { method: POST, body: undefined, expectContentType: true }, + { method: GET, body: undefined, expectContentType: true }, ])( '$method request with body: $body', ({ method, body, expectContentType }) => { @@ -303,18 +312,18 @@ describe('Request Handler', () => { const result = requestHandler.buildConfig(apiUrl, { method, body }); if (expectContentType) { expect(result.headers).toHaveProperty( - 'Content-Type', + CONTENT_TYPE, contentTypeValue, ); } else { - expect(result.headers).not.toHaveProperty('Content-Type'); + expect(result.headers).not.toHaveProperty(CONTENT_TYPE); } }, ); }, ); - describe.each(['DELETE', 'PUT'])( + describe.each([DELETE, PUT])( '%s method with custom Content-Type', (method) => { it(`should keep custom Content-Type for ${method} method`, () => { From e4eb31102a321d5369d72c2521248ef4d7d388f6 Mon Sep 17 00:00:00 2001 From: Red Date: Fri, 15 Nov 2024 15:39:40 +0700 Subject: [PATCH 3/5] chore: Removed DELETE, PUT constants for saving couple of bytes --- src/constants.ts | 1 - src/request-handler.ts | 30 +++++++++++++++--------------- test/request-handler.spec.ts | 12 +++++------- 3 files changed, 20 insertions(+), 23 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index dd0a0be..aca50ce 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -14,6 +14,5 @@ export const CANCELLED_ERROR = 'CanceledError'; export const GET = 'GET'; export const POST = 'POST'; -export const PUT = 'PUT'; export const DELETE = 'DELETE'; export const HEAD = 'HEAD'; diff --git a/src/request-handler.ts b/src/request-handler.ts index 4e67b41..59f5c53 100644 --- a/src/request-handler.ts +++ b/src/request-handler.ts @@ -40,11 +40,9 @@ import { CANCELLED_ERROR, CHARSET_UTF_8, CONTENT_TYPE, - DELETE, GET, HEAD, OBJECT, - PUT, STRING, UNDEFINED, } from './constants'; @@ -180,23 +178,25 @@ export function createRequestHandler( method: string, body?: unknown, ): void => { - if (!body && [PUT, DELETE].includes(method)) { + if (!body && ['PUT', 'DELETE'].includes(method)) { return; - } - - const contentTypeValue = APPLICATION_JSON + ';' + CHARSET_UTF_8; + } else { + const contentTypeValue = APPLICATION_JSON + ';' + CHARSET_UTF_8; - if (headers instanceof Headers) { - if (!headers.has(CONTENT_TYPE)) { - headers.set(CONTENT_TYPE, contentTypeValue); + if (headers instanceof Headers) { + if (!headers.has(CONTENT_TYPE)) { + headers.set(CONTENT_TYPE, contentTypeValue); + } + } else if ( + typeof headers === OBJECT && + !Array.isArray(headers) && + !headers[CONTENT_TYPE] + ) { + headers[CONTENT_TYPE] = contentTypeValue; } - } else if ( - typeof headers === OBJECT && - !Array.isArray(headers) && - !headers[CONTENT_TYPE] - ) { - headers[CONTENT_TYPE] = contentTypeValue; } + + return; }; /** diff --git a/test/request-handler.spec.ts b/test/request-handler.spec.ts index d1c18c0..787b74a 100644 --- a/test/request-handler.spec.ts +++ b/test/request-handler.spec.ts @@ -13,10 +13,8 @@ import { APPLICATION_JSON, CHARSET_UTF_8, CONTENT_TYPE, - DELETE, GET, POST, - PUT, } from '../src/constants'; import { ResponseErr } from '../src/response-error'; @@ -295,10 +293,10 @@ describe('Request Handler', () => { }); describe.each([ - { method: DELETE, body: undefined, expectContentType: false }, - { method: PUT, body: undefined, expectContentType: false }, - { method: DELETE, body: { foo: 'bar' }, expectContentType: true }, - { method: PUT, body: { foo: 'bar' }, expectContentType: true }, + { method: 'DELETE', body: undefined, expectContentType: false }, + { method: 'PUT', body: undefined, expectContentType: false }, + { method: 'DELETE', body: { foo: 'bar' }, expectContentType: true }, + { method: 'PUT', body: { foo: 'bar' }, expectContentType: true }, { method: POST, body: undefined, expectContentType: true }, { method: GET, body: undefined, expectContentType: true }, ])( @@ -323,7 +321,7 @@ describe('Request Handler', () => { }, ); - describe.each([DELETE, PUT])( + describe.each(['DELETE', 'PUT'])( '%s method with custom Content-Type', (method) => { it(`should keep custom Content-Type for ${method} method`, () => { From b7a0b28501cd89e06c9a577d4f696a2e099f0381 Mon Sep 17 00:00:00 2001 From: Red Date: Fri, 15 Nov 2024 15:40:41 +0700 Subject: [PATCH 4/5] fix: Remove Delete from constants file --- src/constants.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/constants.ts b/src/constants.ts index aca50ce..06f9a82 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -14,5 +14,4 @@ export const CANCELLED_ERROR = 'CanceledError'; export const GET = 'GET'; export const POST = 'POST'; -export const DELETE = 'DELETE'; export const HEAD = 'HEAD'; From b5f5a9e4dcace0589540bbf20a71d0312ca80e3f Mon Sep 17 00:00:00 2001 From: Red Date: Fri, 15 Nov 2024 15:42:29 +0700 Subject: [PATCH 5/5] fix: Remove POST from constants file --- src/constants.ts | 1 - test/request-handler.spec.ts | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index 06f9a82..bd4d045 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -13,5 +13,4 @@ export const TIMEOUT_ERROR = 'TimeoutError'; export const CANCELLED_ERROR = 'CanceledError'; export const GET = 'GET'; -export const POST = 'POST'; export const HEAD = 'HEAD'; diff --git a/test/request-handler.spec.ts b/test/request-handler.spec.ts index 787b74a..436db2c 100644 --- a/test/request-handler.spec.ts +++ b/test/request-handler.spec.ts @@ -14,7 +14,6 @@ import { CHARSET_UTF_8, CONTENT_TYPE, GET, - POST, } from '../src/constants'; import { ResponseErr } from '../src/response-error'; @@ -297,7 +296,7 @@ describe('Request Handler', () => { { method: 'PUT', body: undefined, expectContentType: false }, { method: 'DELETE', body: { foo: 'bar' }, expectContentType: true }, { method: 'PUT', body: { foo: 'bar' }, expectContentType: true }, - { method: POST, body: undefined, expectContentType: true }, + { method: 'POST', body: undefined, expectContentType: true }, { method: GET, body: undefined, expectContentType: true }, ])( '$method request with body: $body',