From a5d3d9619c58f6f9eebb7dff693e90faa0026b35 Mon Sep 17 00:00:00 2001 From: Naveen S Date: Thu, 10 Apr 2025 13:13:30 +0530 Subject: [PATCH 1/5] fix: renamed org full page to global full page --- __test__/organizationFullPage.test.ts | 16 +-- __test__/uiLocation.test.ts | 154 +++++++++++++++----------- package-lock.json | 2 +- src/types.ts | 22 ++-- src/uiLocation.ts | 10 +- 5 files changed, 121 insertions(+), 83 deletions(-) diff --git a/__test__/organizationFullPage.test.ts b/__test__/organizationFullPage.test.ts index 75b6350..34ead95 100644 --- a/__test__/organizationFullPage.test.ts +++ b/__test__/organizationFullPage.test.ts @@ -1,13 +1,13 @@ -import { IOrgFullPageLocationInitData, LocationType } from "../src/types"; +import { IGlobalFullPageLocationInitData, LocationType } from "../src/types"; import { OrganizationDetails } from "../src/types/organization.types"; -const mockData: IOrgFullPageLocationInitData = { - type: LocationType.ORGANIZATION_FULL_PAGE, +const mockData: IGlobalFullPageLocationInitData = { + type: LocationType.GLOBAL_FULL_PAGE_LOCATION, app_id: "app_id", installation_uid: "installation_uid", extension_uid: "extension_uid", region: "NA", - endpoints:{CMA:"",APP:"",DEVELOPER_HUB:""}, + endpoints: { CMA: "", APP: "", DEVELOPER_HUB: "" }, stack: {} as any, user: {} as any, currentBranch: "currentBranch", @@ -22,11 +22,13 @@ afterEach(() => { }); test("should return organization details", () => { - expect(organizationFullPage.currentOrganization).toBe(mockData.organization); + expect(organizationFullPage.currentOrganization).toBe( + mockData.organization + ); }); test("should handle missing organization details", () => { - const invalidData: IOrgFullPageLocationInitData = { + const invalidData: IGlobalFullPageLocationInitData = { ...mockData, organization: null as any, // check missing organization details }; @@ -34,4 +36,4 @@ test("should handle missing organization details", () => { currentOrganization: invalidData.organization, }; expect(invalidOrganizationFullPage.currentOrganization).toBeNull(); -}); \ No newline at end of file +}); diff --git a/__test__/uiLocation.test.ts b/__test__/uiLocation.test.ts index ebdacb1..2017748 100644 --- a/__test__/uiLocation.test.ts +++ b/__test__/uiLocation.test.ts @@ -1,3 +1,5 @@ +import { AxiosRequestConfig, AxiosResponse } from "axios"; + import postRobot from "post-robot"; import UiLocation from "../src/uiLocation"; @@ -12,9 +14,8 @@ import { LocationType, Region, } from "../src/types"; -import { RequestOption } from '../src/types/common.types'; -import { RequestConfig } from '../src/types/api.type'; -import { AxiosRequestConfig, AxiosResponse } from 'axios'; +import { RequestOption } from "../src/types/common.types"; +import { RequestConfig } from "../src/types/api.type"; jest.mock("post-robot"); jest.mock("wolfy87-eventemitter"); @@ -60,7 +61,11 @@ const initData: IAppConfigInitData = { installation_uid: "installation_uid", extension_uid: "extension_uid", region: "NA", - endpoints: { CMA: "https://api.contentstack.io", APP: "https://app.contentstack.app",DEVELOPER_HUB:"" }, + endpoints: { + CMA: "https://api.contentstack.io", + APP: "https://app.contentstack.app", + DEVELOPER_HUB: "", + }, stack: mockStackData, user: {} as any, currentBranch: "currentBranch", @@ -135,7 +140,7 @@ describe("UI Location", () => { }); }); - describe('dispatchPostRobotRequest', () => { + describe("dispatchPostRobotRequest", () => { let mockPostRobot: typeof postRobot; let opts: RequestOption; let uiLocationInstance: UiLocation; @@ -143,35 +148,40 @@ describe("UI Location", () => { beforeEach(() => { mockPostRobot = postRobot; - opts = { method: 'GET' }; + opts = { method: "GET" }; uiLocationInstance = new UiLocation(initData); onError = jest.fn(); uiLocationInstance.api = jest.fn().mockResolvedValue({ - method: 'GET', - url: "https://test.com/test?limit=10&skip=0" + method: "GET", + url: "https://test.com/test?limit=10&skip=0", }); }); - it('should call sendToParent with the correct arguments and resolve with data', async () => { + it("should call sendToParent with the correct arguments and resolve with data", async () => { const mockData = { success: true }; // Call the method that uses uiLocationInstance.api - const result = await uiLocationInstance.api("https://test.com/test?limit=10&skip=0",{ - method: 'GET' - }); + const result = await uiLocationInstance.api( + "https://test.com/test?limit=10&skip=0", + { + method: "GET", + } + ); // Assertions - expect(uiLocationInstance.api).toHaveBeenCalledWith('https://test.com/test?limit=10&skip=0',{ - method: 'GET' - }); + expect(uiLocationInstance.api).toHaveBeenCalledWith( + "https://test.com/test?limit=10&skip=0", + { + method: "GET", + } + ); expect(result).toEqual({ - method: 'GET', - url: 'https://test.com/test?limit=10&skip=0', + method: "GET", + url: "https://test.com/test?limit=10&skip=0", }); - }); - it('should call onError if sendToParent rejects', async () => { - const mockError = new Error('Test error'); + it("should call onError if sendToParent rejects", async () => { + const mockError = new Error("Test error"); // Mock the api method to reject with an error uiLocationInstance.api = jest.fn().mockRejectedValue(mockError); @@ -182,84 +192,102 @@ describe("UI Location", () => { }); // Call the method that uses uiLocationInstance.api and expect it to throw an error - await expect(uiLocationInstance.api("https://test.com/test?limit=10&skip=0",{ - method: 'GET' - })).rejects.toThrow('Test error'); + await expect( + uiLocationInstance.api( + "https://test.com/test?limit=10&skip=0", + { + method: "GET", + } + ) + ).rejects.toThrow("Test error"); }); }); - describe("createSDKAdapter", () => { let mockPostRobot: typeof postRobot; let opts: RequestConfig; let uiLocationInstance: UiLocation; let onError: jest.Mock; - + beforeEach(() => { mockPostRobot = postRobot; - opts = { method: 'GET', baseURL: "https://test.com", url: "/test?limit10&skip=0" }; + opts = { + method: "GET", + baseURL: "https://test.com", + url: "/test?limit10&skip=0", + }; uiLocationInstance = new UiLocation(initData); onError = jest.fn(); - uiLocationInstance.createAdapter = jest.fn().mockImplementation(() => async (config: AxiosRequestConfig) => { - return { - method: 'GET', - url: '/test?limit=10&skip=0', - baseURL: 'https://test.com', - data: {} - } as unknown as AxiosResponse; - }); + uiLocationInstance.createAdapter = jest + .fn() + .mockImplementation( + () => async (config: AxiosRequestConfig) => { + return { + method: "GET", + url: "/test?limit=10&skip=0", + baseURL: "https://test.com", + data: {}, + } as unknown as AxiosResponse; + } + ); }); - + afterEach(() => { postRobotOnMock.mockClear(); postRobotSendToParentMock.mockClear(); - + jest.clearAllMocks(); window["postRobot"] = undefined; window["iframeRef"] = undefined; }); - - it('should call createAdapter with the correct arguments and resolve with data', async () => { + + it("should call createAdapter with the correct arguments and resolve with data", async () => { const mockData = { success: true }; // Call the method that uses uiLocationInstance.createAdapter const result = await uiLocationInstance.createAdapter()({ - method: 'GET', - url: '/test?limit=10&skip=0', - baseURL: 'https://test.com', - data: {} + method: "GET", + url: "/test?limit=10&skip=0", + baseURL: "https://test.com", + data: {}, }); - + expect(result).toEqual({ - method: 'GET', - url: '/test?limit=10&skip=0', - baseURL: 'https://test.com', - data: {} + method: "GET", + url: "/test?limit=10&skip=0", + baseURL: "https://test.com", + data: {}, }); }); - - it('should call onError if createAdapter rejects', async () => { - const mockError = new Error('Test error'); - + + it("should call onError if createAdapter rejects", async () => { + const mockError = new Error("Test error"); + // Mock the createAdapter method to reject with an error - uiLocationInstance.createAdapter = jest.fn().mockImplementation(() => async (config: AxiosRequestConfig) => { - throw mockError; - }); - + uiLocationInstance.createAdapter = jest + .fn() + .mockImplementation( + () => async (config: AxiosRequestConfig) => { + throw mockError; + } + ); + // Mock the onError implementation onError.mockImplementation((error) => { throw error; }); - + // Call the method that uses uiLocationInstance.createAdapter and expect it to throw an error - await expect(uiLocationInstance.createAdapter()({ - method: 'GET', - url: '/test?limit=10&skip=0', - baseURL: 'https://test.com', - data: {} - })).rejects.toThrow('Test error'); + await expect( + uiLocationInstance.createAdapter()({ + method: "GET", + url: "/test?limit=10&skip=0", + baseURL: "https://test.com", + data: {}, + }) + ).rejects.toThrow("Test error"); }); }); - + describe("getConfig", () => { it("should return config if no installation uid present", async () => { const uiLocation = new UiLocation(initDataJsonRte as any); diff --git a/package-lock.json b/package-lock.json index 9be09aa..03a3bf9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,7 +46,7 @@ "webpack-dev-server": "^4.7.3" }, "engines": { - "node": "18.x" + "node": ">=18.x" } }, "node_modules/@adobe/css-tools": { diff --git a/src/types.ts b/src/types.ts index c71b513..c351baa 100644 --- a/src/types.ts +++ b/src/types.ts @@ -7,7 +7,7 @@ import { GenericObjectType } from "./types/common.types"; import { Entry } from "./types/entry.types"; import { Asset, ContentType, Schema, StackDetail } from "./types/stack.types"; import { OrganizationDetails } from "./types/organization.types"; -import { ContentstackEndpoints } from './types/api.type'; +import { ContentstackEndpoints } from "./types/api.type"; import { User } from "./types/user.types"; import Window from "./window"; @@ -69,7 +69,7 @@ export declare interface IAppConfigWidget { stack: Stack; } -export declare interface IOrgFullPageLocation { +export declare interface IGlobalFullPageLocation { currentOrganization: OrganizationDetails; } @@ -88,7 +88,7 @@ export enum LocationType { RTE = "RTE", WIDGET = "WIDGET", CONTENT_TYPE_SIDEBAR_WIDGET = "CONTENT_TYPE_SIDEBAR_WIDGET", - ORGANIZATION_FULL_PAGE = "ORGANIZATION_FULL_PAGE", + GLOBAL_FULL_PAGE_LOCATION = "GLOBAL_FULL_PAGE_LOCATION", } // Init data @@ -106,10 +106,11 @@ declare interface ICommonInitData { endpoints: ContentstackEndpoints; } -export declare interface IOrgFullPageLocationInitData extends ICommonInitData { +export declare interface IGlobalFullPageLocationInitData + extends ICommonInitData { organization: OrganizationDetails; config?: GenericObjectType; - type: LocationType.ORGANIZATION_FULL_PAGE; + type: LocationType.GLOBAL_FULL_PAGE_LOCATION; } export declare interface IDashboardInitData extends ICommonInitData { @@ -217,7 +218,7 @@ export type InitializationData = | IRTEInitData | ISidebarInitData | IContentTypeSidebarInitData - | IOrgFullPageLocationInitData; + | IGlobalFullPageLocationInitData; /** * installation details API response @@ -259,4 +260,11 @@ export enum Region { GCP_EU = "GCP_EU", } -export type RegionType = "UNKNOWN" | "NA" | "EU" | "AZURE_NA" | "AZURE_EU" | "GCP_NA" | string; \ No newline at end of file +export type RegionType = + | "UNKNOWN" + | "NA" + | "EU" + | "AZURE_NA" + | "AZURE_EU" + | "GCP_NA" + | string; diff --git a/src/uiLocation.ts b/src/uiLocation.ts index 443aca9..5507b49 100755 --- a/src/uiLocation.ts +++ b/src/uiLocation.ts @@ -27,7 +27,7 @@ import { InitializationData, LocationType, Manifest, - IOrgFullPageLocation, + IGlobalFullPageLocation, RegionType, } from "./types"; import { GenericObjectType } from "./types/common.types"; @@ -132,7 +132,7 @@ class UiLocation { FullPage: IFullPageLocation | null; FieldModifierLocation: IFieldModifierLocation | null; ContentTypeSidebarWidget: ContentTypeSidebarWidget | null; - OrganizationFullPage: IOrgFullPageLocation | null; + GlobalFullPageLocation: IGlobalFullPageLocation | null; }; constructor(initData: InitializationData) { @@ -180,7 +180,7 @@ class UiLocation { FullPage: null, FieldModifierLocation: null, ContentTypeSidebarWidget: null, - OrganizationFullPage: null, + GlobalFullPageLocation: null, }; window["postRobot"] = postRobot; @@ -289,8 +289,8 @@ class UiLocation { break; } - case LocationType.ORGANIZATION_FULL_PAGE: { - this.location.OrganizationFullPage = { + case LocationType.GLOBAL_FULL_PAGE_LOCATION: { + this.location.GlobalFullPageLocation = { currentOrganization: initializationData.organization, }; break; From a102ed8c61a90c760cbb0dee2002f5448f5420b9 Mon Sep 17 00:00:00 2001 From: Amitkanswal Date: Mon, 12 May 2025 14:00:16 +0530 Subject: [PATCH 2/5] fix:api method body response fix --- package-lock.json | 10 ++++---- package.json | 2 +- src/utils/adapter.ts | 10 +++----- src/utils/utils.ts | 54 +++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 62 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index 03a3bf9..99a0c72 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@contentstack/app-sdk", - "version": "2.3.0", + "version": "2.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@contentstack/app-sdk", - "version": "2.3.0", + "version": "2.3.1", "license": "MIT", "dependencies": { "axios": "^1.7.9", @@ -6522,9 +6522,9 @@ } }, "node_modules/http-proxy-middleware": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz", - "integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==", + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", + "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", "dev": true, "dependencies": { "@types/http-proxy": "^1.17.8", diff --git a/package.json b/package.json index 1a86bee..30f8cff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@contentstack/app-sdk", - "version": "2.3.0", + "version": "2.3.1", "types": "dist/src/index.d.ts", "description": "The Contentstack App SDK allows you to customize your Contentstack applications.", "main": "dist/index.js", diff --git a/src/utils/adapter.ts b/src/utils/adapter.ts index dc829d3..39349ff 100644 --- a/src/utils/adapter.ts +++ b/src/utils/adapter.ts @@ -6,7 +6,7 @@ import { AxiosResponse, } from "axios"; -import { fetchToAxiosConfig } from "./utils"; +import { axiosToFetchResponse, fetchToAxiosConfig, sanitizeFetchResponseHeader } from "./utils"; /** * Dispatches a request using PostRobot. @@ -60,15 +60,11 @@ export const dispatchApiRequest = async ( ): Promise => { try { const config = fetchToAxiosConfig(url, options); - const response = (await dispatchAdapter(PostRobot)( + const axiosResponse = (await dispatchAdapter(PostRobot)( config )) as AxiosResponse; - return new Response(response?.data, { - status: response.status, - statusText: response.statusText, - headers: response.config.headers, - }); + return axiosToFetchResponse(axiosResponse, sanitizeFetchResponseHeader); } catch (err: any) { if (err.response) { return new Response(err.response?.data, { diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 97a4d0b..e918f32 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,4 +1,4 @@ -import { AxiosHeaders, AxiosRequestConfig } from "axios"; +import { AxiosHeaders, AxiosRequestConfig, AxiosResponse } from "axios"; import { Region, RegionType } from "../types"; @@ -85,3 +85,55 @@ export const fetchToAxiosConfig = ( return axiosConfig; }; + +export function axiosToFetchResponse( + axiosRes: AxiosResponse, + sanitizeHeadersFn?: (headers: Record) => Record + ): Response { + const { data, status, statusText, headers: rawHeaders } = axiosRes; + + let body: BodyInit; + let contentType = 'application/json'; + + if (data instanceof Blob || typeof data === 'string' || data instanceof ArrayBuffer) { + body = data; + contentType = rawHeaders['content-type'] || 'application/octet-stream'; + } else { + body = JSON.stringify(data); + } + + const headersObj = sanitizeHeadersFn + ? sanitizeHeadersFn( + Object.fromEntries( + Object.entries(rawHeaders).map(([key, value]) => [key, String(value)]) + ) + ) + : Object.fromEntries( + Object.entries(rawHeaders).map(([key, value]) => [key, String(value)]) + ); + + if (!headersObj['content-type']) { + headersObj['content-type'] = contentType; + } + + const responseInit: ResponseInit = { + status, + statusText, + headers: new Headers(headersObj), + }; + + return new Response(body, responseInit); + } + + export function sanitizeFetchResponseHeader(headers: Record): Record { + const blocked = ['authorization', 'cookie', 'set-cookie']; + const sanitized: Record = {}; + + for (const [key, value] of Object.entries(headers)) { + if (!blocked.includes(key.toLowerCase())) { + sanitized[key] = value; + } + } + + return sanitized; + } \ No newline at end of file From 6e97f8c22206fc9e25e497a35495aab9fe363ffd Mon Sep 17 00:00:00 2001 From: Amitkanswal Date: Mon, 12 May 2025 15:18:53 +0530 Subject: [PATCH 3/5] fix:removed cookies and authorization check --- src/utils/adapter.ts | 4 +-- src/utils/utils.ts | 68 ++++++++++++++++---------------------------- 2 files changed, 27 insertions(+), 45 deletions(-) diff --git a/src/utils/adapter.ts b/src/utils/adapter.ts index 39349ff..b96fb4c 100644 --- a/src/utils/adapter.ts +++ b/src/utils/adapter.ts @@ -6,7 +6,7 @@ import { AxiosResponse, } from "axios"; -import { axiosToFetchResponse, fetchToAxiosConfig, sanitizeFetchResponseHeader } from "./utils"; +import { axiosToFetchResponse, fetchToAxiosConfig } from "./utils"; /** * Dispatches a request using PostRobot. @@ -64,7 +64,7 @@ export const dispatchApiRequest = async ( config )) as AxiosResponse; - return axiosToFetchResponse(axiosResponse, sanitizeFetchResponseHeader); + return axiosToFetchResponse(axiosResponse); } catch (err: any) { if (err.response) { return new Response(err.response?.data, { diff --git a/src/utils/utils.ts b/src/utils/utils.ts index e918f32..edb7e39 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -86,54 +86,36 @@ export const fetchToAxiosConfig = ( return axiosConfig; }; -export function axiosToFetchResponse( - axiosRes: AxiosResponse, - sanitizeHeadersFn?: (headers: Record) => Record - ): Response { +export function axiosToFetchResponse(axiosRes: AxiosResponse): Response { const { data, status, statusText, headers: rawHeaders } = axiosRes; - + let body: BodyInit; - let contentType = 'application/json'; - - if (data instanceof Blob || typeof data === 'string' || data instanceof ArrayBuffer) { - body = data; - contentType = rawHeaders['content-type'] || 'application/octet-stream'; + let contentType = "application/json"; + + if ( + data instanceof Blob || + typeof data === "string" || + data instanceof ArrayBuffer + ) { + body = data; + contentType = rawHeaders["content-type"] || "application/octet-stream"; } else { - body = JSON.stringify(data); + body = JSON.stringify(data); } - - const headersObj = sanitizeHeadersFn - ? sanitizeHeadersFn( - Object.fromEntries( - Object.entries(rawHeaders).map(([key, value]) => [key, String(value)]) - ) - ) - : Object.fromEntries( - Object.entries(rawHeaders).map(([key, value]) => [key, String(value)]) - ); - - if (!headersObj['content-type']) { - headersObj['content-type'] = contentType; + + const headersObj = Object.fromEntries( + Object.entries(rawHeaders).map(([key, value]) => [key, String(value)]) + ); + + if (!headersObj["content-type"]) { + headersObj["content-type"] = contentType; } - + const responseInit: ResponseInit = { - status, - statusText, - headers: new Headers(headersObj), + status, + statusText, + headers: new Headers(headersObj), }; - + return new Response(body, responseInit); - } - - export function sanitizeFetchResponseHeader(headers: Record): Record { - const blocked = ['authorization', 'cookie', 'set-cookie']; - const sanitized: Record = {}; - - for (const [key, value] of Object.entries(headers)) { - if (!blocked.includes(key.toLowerCase())) { - sanitized[key] = value; - } - } - - return sanitized; - } \ No newline at end of file +} From 6e1cbf484558ccb163afa8dfc93922bc72c6bfa1 Mon Sep 17 00:00:00 2001 From: Amitkanswal Date: Mon, 12 May 2025 15:52:54 +0530 Subject: [PATCH 4/5] fix:header header response Signed-off-by: Amitkanswal --- src/utils/utils.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/utils/utils.ts b/src/utils/utils.ts index edb7e39..a81e1bc 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -87,7 +87,11 @@ export const fetchToAxiosConfig = ( }; export function axiosToFetchResponse(axiosRes: AxiosResponse): Response { - const { data, status, statusText, headers: rawHeaders } = axiosRes; + const { data, status, statusText, config } = axiosRes; + + const headersObj = Object.fromEntries( + Object.entries(config.headers).map(([key, value]) => [key, String(value)]) + ); let body: BodyInit; let contentType = "application/json"; @@ -98,15 +102,11 @@ export function axiosToFetchResponse(axiosRes: AxiosResponse): Response { data instanceof ArrayBuffer ) { body = data; - contentType = rawHeaders["content-type"] || "application/octet-stream"; + contentType = config.headers["content-type"] || "application/octet-stream"; } else { body = JSON.stringify(data); } - const headersObj = Object.fromEntries( - Object.entries(rawHeaders).map(([key, value]) => [key, String(value)]) - ); - if (!headersObj["content-type"]) { headersObj["content-type"] = contentType; } From 5a8bd805d6b117728387ba0e9dfd3253103b2bf2 Mon Sep 17 00:00:00 2001 From: Amitkanswal Date: Mon, 12 May 2025 15:59:22 +0530 Subject: [PATCH 5/5] fix:refactor headers logic Signed-off-by: Amitkanswal --- src/utils/utils.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/utils/utils.ts b/src/utils/utils.ts index a81e1bc..7f14cdf 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -89,10 +89,6 @@ export const fetchToAxiosConfig = ( export function axiosToFetchResponse(axiosRes: AxiosResponse): Response { const { data, status, statusText, config } = axiosRes; - const headersObj = Object.fromEntries( - Object.entries(config.headers).map(([key, value]) => [key, String(value)]) - ); - let body: BodyInit; let contentType = "application/json"; @@ -107,14 +103,14 @@ export function axiosToFetchResponse(axiosRes: AxiosResponse): Response { body = JSON.stringify(data); } - if (!headersObj["content-type"]) { - headersObj["content-type"] = contentType; + if (!config.headers["content-type"]) { + config.headers["content-type"] = contentType; } const responseInit: ResponseInit = { status, statusText, - headers: new Headers(headersObj), + headers: config.headers, }; return new Response(body, responseInit);