diff --git a/README.md b/README.md index 96d2671ab..9ebb86ac0 100644 --- a/README.md +++ b/README.md @@ -192,3 +192,5 @@ Secrets scrubbing are subject to the following limitations: * Only JSON data secrets scrubbing is supported + + diff --git a/jest.config.js b/jest.config.js index 923885227..cd0bffd31 100644 --- a/jest.config.js +++ b/jest.config.js @@ -17,6 +17,7 @@ module.exports = () => { // fetch is not supported in Node.js versions lower than 18, // so no need to check coverage for that version collectCoverageFrom.push('!./src/hooks/fetch.ts'); + collectCoverageFrom.push('!./src/hooks/baseFetch.ts'); } if (NODE_MAJOR_VERSION > 14) { diff --git a/src/hooks/baseFetch.test.ts b/src/hooks/baseFetch.test.ts new file mode 100644 index 000000000..dcde02f4f --- /dev/null +++ b/src/hooks/baseFetch.test.ts @@ -0,0 +1,229 @@ +import { NODE_MAJOR_VERSION } from '../../testUtils/nodeVersion'; +import { BaseFetch } from './baseFetch'; + +// Conditionally skip the test suite +// if (NODE_MAJOR_VERSION < 18) { +// describe.skip('fetchUtils.skip', () => { +// it('should not run on Node.js versions less than 18', () => { +// // test cases here +// }); +// }); +// } else { +describe('fetchUtils', () => { + test('Test convertHeadersToKeyValuePairs - Headers input', () => { + // @ts-ignore + const headers = new Headers({ + 'content-type': 'application/json', + 'content-length': '12345', + }); + // @ts-ignore + const parsedHeaders = BaseFetch._convertHeadersToKeyValuePairs(headers); + expect(parsedHeaders).toEqual({ + 'content-type': 'application/json', + 'content-length': '12345', + }); + }); + + test('Test convertHeadersToKeyValuePairs - Record input', () => { + const headers = { + 'content-type': 'application/json', + 'content-length': '12345', + }; + // @ts-ignore + const parsedHeaders = BaseFetch._convertHeadersToKeyValuePairs(headers); + expect(parsedHeaders).toEqual({ + 'content-type': 'application/json', + 'content-length': '12345', + }); + }); + + test('Test convertHeadersToKeyValuePairs - string[][] input', () => { + const headers = [ + ['content-type', 'application/json'], + ['content-length', '12345'], + ]; + // @ts-ignore + const parsedHeaders = BaseFetch._convertHeadersToKeyValuePairs(headers); + expect(parsedHeaders).toEqual({ + 'content-type': 'application/json', + 'content-length': '12345', + }); + }); + + // TODO: Test the parseRequestArguments func + test.each([ + [ + { + input: 'https://example.com', + init: undefined, + expectedUrl: 'https://example.com', + expectedOptions: { + method: 'GET', + headers: {}, + }, + }, + ], + [ + { + input: new URL('https://example.com'), + init: undefined, + expectedUrl: 'https://example.com/', + expectedOptions: { + method: 'GET', + headers: {}, + }, + }, + ], + [ + { + input: new URL('https://example.com/'), + init: undefined, + expectedUrl: 'https://example.com/', + expectedOptions: { + method: 'GET', + headers: {}, + }, + }, + ], + [ + { + // @ts-ignore + input: new Request('https://example.com'), + init: undefined, + expectedUrl: 'https://example.com/', + expectedOptions: { + method: 'GET', + headers: {}, + }, + }, + ], + [ + { + // @ts-ignore + input: new Request(new URL('https://example.com')), + init: undefined, + expectedUrl: 'https://example.com/', + expectedOptions: { + method: 'GET', + headers: {}, + }, + }, + ], + [ + { + // @ts-ignore + input: new Request('https://example.com', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ data: '12345' }), + }), + init: undefined, + expectedUrl: 'https://example.com/', + expectedOptions: { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ data: '12345' }), + }, + }, + ], + // Here we will add the init object, making sure it overrides the input values + [ + { + // @ts-ignore + input: new Request('https://example.com', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ data: '12345' }), + }), + init: { + method: 'PUT', + headers: { 'content-type': 'application/xml' }, + body: JSON.stringify({ data: '54321' }), + }, + expectedUrl: 'https://example.com/', + expectedOptions: { + method: 'PUT', + headers: { 'content-type': 'application/xml' }, + body: JSON.stringify({ data: '54321' }), + }, + }, + ], + [ + { + input: 'https://example.com', + init: { + method: 'PUT', + headers: { 'content-type': 'application/xml' }, + body: JSON.stringify({ data: '54321' }), + }, + expectedUrl: 'https://example.com', + expectedOptions: { + method: 'PUT', + headers: { 'content-type': 'application/xml' }, + body: JSON.stringify({ data: '54321' }), + }, + }, + ], + [ + { + input: new URL('https://example.com'), + init: { + method: 'PUT', + headers: { 'content-type': 'application/xml' }, + body: JSON.stringify({ data: '54321' }), + }, + expectedUrl: 'https://example.com/', + expectedOptions: { + method: 'PUT', + headers: { 'content-type': 'application/xml' }, + body: JSON.stringify({ data: '54321' }), + }, + }, + ], + // Test different formats for body + [ + { + input: 'https://example.com', + init: { + method: 'PUT', + headers: { 'content-type': 'application/xml' }, + // @ts-ignore + body: new Blob(['Blob contents']), + }, + expectedUrl: 'https://example.com', + expectedOptions: { + method: 'PUT', + headers: { 'content-type': 'application/xml' }, + body: 'Blob contents', + }, + }, + ], + [ + { + input: 'https://example.com', + init: { + method: 'PUT', + headers: { 'content-type': 'application/xml' }, + // Unsupported body type + body: 123, + }, + expectedUrl: 'https://example.com', + expectedOptions: { + method: 'PUT', + headers: { 'content-type': 'application/xml' }, + }, + }, + ], + // TODO: Test FormData body + // TODO: Test ReadableStream body + ])( + 'Test parsing fetch command arguments: %p', + async ({ input, init, expectedUrl, expectedOptions }) => { + // @ts-ignore + const { url, options } = await BaseFetch._parseRequestArguments({ input, init }); + expect(url).toEqual(expectedUrl); + expect(options).toEqual(expectedOptions); + } + ); +}); +// } diff --git a/src/hooks/baseFetch.ts b/src/hooks/baseFetch.ts new file mode 100644 index 000000000..1ca62a5e3 --- /dev/null +++ b/src/hooks/baseFetch.ts @@ -0,0 +1,212 @@ +import { ParseHttpRequestOptions, RequestData, UrlAndRequestOptions } from './baseHttp'; +import * as logger from '../logger'; + +export interface ResponseData { + headers?: Record; + statusCode?: number; + body?: string; + // The time when all the response data was received, including all the body chunks + receivedTime?: number; + truncated?: boolean; + isNetworkError?: boolean; +} + +export interface RequestExtenderContext { + isTracedDisabled?: boolean; + awsRequestId?: string; + transactionId?: string; + requestRandomId?: string; + currentSpan?: any; + requestData?: RequestData; + // @ts-ignore + response?: Response; +} + +export interface FetchArguments { + // @ts-ignore + input: RequestInfo | URL; + // @ts-ignore + init?: RequestInit; +} + +export type FetchUrlAndRequestOptions = UrlAndRequestOptions & { + options: ParseHttpRequestOptions & { + body?: string; + }; +}; + +export class BaseFetch { + constructor() { + if (new.target === BaseFetch) { + throw new Error('Cannot instantiate class.'); + } + } + + /** + * Parses the raw arguments passed to the fetch function and returns the URL and options object. + * @param {FetchArguments} args The raw fetch arguments + * @param {RequestInfo | URL} args.input The first argument (called input / resource) that is passed to the fetch command + * @param {RequestInit} args.init The second argument (called init / options) that is passed to the fetch command + * @returns {UrlAndRequestOptions} Our custom request options object containing the URL and options + * @protected + */ + static async _parseRequestArguments({ + input, + init, + }: FetchArguments): Promise { + let url: string = undefined; + const options: ParseHttpRequestOptions & { + body?: string; + } = { + headers: {}, + method: 'GET', + }; + + if (input instanceof URL) { + url = input.toString(); + } else if (typeof input === 'string') { + url = input; + // @ts-ignore + } else if (input instanceof Request) { + url = input.url; + options.method = input.method || 'GET'; + options.headers = BaseFetch._convertHeadersToKeyValuePairs(input.headers); + } + + if (init) { + options.method = init.method || options.method || 'GET'; + options.headers = { + ...options.headers, + ...BaseFetch._convertHeadersToKeyValuePairs(init.headers), + }; + } + + // Read the body from the request object, only if we shouldn't look in the init object + let body: string = undefined; + try { + // @ts-ignore + if (input instanceof Request && input.body && !init?.body) { + body = await input.clone().text(); + } + } catch (e) { + logger.debug('Failed to read body from Request object', e); + } + + // If we didn't get the body from the request object, get it from the init object + if (!body && init?.body) { + try { + // @ts-ignore + const decoder = new TextDecoder(); + // @ts-ignore + if (init.body instanceof ReadableStream) { + const reader = init.body.getReader(); + let result = ''; + // Limiting the number of reads to prevent an infinite read loop + for (let i = 0; i < 10000; i++) { + const { done, value } = await reader.read(); + if (done) { + break; + } + result += decoder.decode(value); + } + body = result; + // @ts-ignore + } else if (init.body instanceof Blob) { + body = await init.body.text(); + } else if (init.body instanceof ArrayBuffer) { + body = decoder.decode(init.body); + } else if (typeof init.body === 'string') { + body = init.body; + } else { + // TODO: Implement FormData support + logger.debug('Unsupported request body type', typeof init.body); + } + } catch (e) { + logger.debug('Failed to read request body from Request object', { + error: e, + bodyObjectType: typeof init.body, + }); + } + } + + if (body) { + options.body = body; + } + + return { url, options }; + } + + /** + * Converts the headers object to a key-value pair object. + * Fetch library uses multiple format to represent headers, this function will convert them all to a key-value pair object. + * @param {[string, string][] | Record | Headers} headers Headers object as used by the fetch library + * @returns {Record} The headers as a key-value pair object + * @protected + */ + static _convertHeadersToKeyValuePairs( + // @ts-ignore + headers: [string, string][] | Record | Headers + ): Record { + // @ts-ignore + if (headers instanceof Headers) { + const headersObject: Record = {}; + headers.forEach((value, key) => { + headersObject[key] = value; + }); + return headersObject; + } + if (Array.isArray(headers)) { + const headersObject: Record = {}; + headers.forEach(([key, value]) => { + headersObject[key] = value; + }); + return headersObject; + } + + return headers; + } + + /** + * Adds the headers found in the options object to the fetch arguments, and return the modified arguments. + * The original arguments will not be modified. + * @param {FetchArguments} args The original fetch arguments + * @param {ParseHttpRequestOptions} args.input The first argument (called input / resource) that is passed to the fetch command + * @param {RequestInit} args.init The second argument (called init / options) that is passed to the fetch command + * @param {ParseHttpRequestOptions} args.options Our custom request options object containing the headers to add to the fetch arguments + * @returns {FetchArguments} The modified fetch arguments with the headers added from the args.options object + */ + static _addHeadersToFetchArguments({ + input, + init, + options, + }: FetchArguments & { options: ParseHttpRequestOptions }): FetchArguments { + // The init headers take precedence over the input headers + // @ts-ignore + const newInit: RequestInit = init ? { ...init } : {}; + + if (options.headers) { + const currentHeaders = newInit.headers || {}; + newInit.headers = { ...currentHeaders, ...options.headers }; + } + + return { input, init: newInit }; + } + + /** + * Converts the fetch response object instance to a custom response data object used by the rest of the lumigo tracer codebase. + * @param {Response} response + * @returns {ResponseData} + * @protected + */ + // @ts-ignore + static _convertResponseToResponseData(response: Response): ResponseData { + const headers = {}; + response.headers.forEach((value, key) => { + headers[key] = value; + }); + return { + headers, + statusCode: response.status, + }; + } +} diff --git a/src/hooks/fetch.test.ts b/src/hooks/fetch.test.ts index 5591826bc..4fa8561f7 100644 --- a/src/hooks/fetch.test.ts +++ b/src/hooks/fetch.test.ts @@ -65,7 +65,10 @@ describe('fetch', () => { test.each([...cases])( 'Test basic http span creation: %p', async ({ method, protocol, host, reqHeaders, reqBody, resStatusCode, resHeaders, resBody }) => { - fetchMock.mockResponseOnce(resBody, { + // Handle 304 or other status codes that shouldn't have a body + const bodyForMock = resStatusCode === 304 ? null : resBody; + + fetchMock.mockResponseOnce(bodyForMock, { status: resStatusCode, headers: resHeaders, }); @@ -80,7 +83,8 @@ describe('fetch', () => { body: reqBody, }); const body = await response.text(); - if (resBody === undefined) { + // Ensure that a 304 or similar response has no body + if (resBody === undefined || resStatusCode === 304) { expect(body).toEqual(''); } else { expect(body).toEqual(resBody); @@ -124,10 +128,12 @@ describe('fetch', () => { expect(responseData.statusCode).toEqual(resStatusCode); expect(responseData.headers).toEqual(responseHeaders); expect(responseData.headers).toEqual(resHeaders); - if (resBody === undefined) { - expect(responseData.body).toEqual(''); + + // Ensure that a 304 or similar response has no body + if (resBody === undefined || resStatusCode === 304) { + expect(body).toEqual(''); } else { - expect(responseData.body).toEqual(resBody); + expect(body).toEqual(resBody); } } ); @@ -444,222 +450,6 @@ describe('fetch', () => { expect(responseData.body).toEqual(responseBody); }); - // TODO: Test the parseRequestArguments func - test.each([ - [ - { - input: 'https://example.com', - init: undefined, - expectedUrl: 'https://example.com', - expectedOptions: { - method: 'GET', - headers: {}, - }, - }, - ], - [ - { - input: new URL('https://example.com'), - init: undefined, - expectedUrl: 'https://example.com/', - expectedOptions: { - method: 'GET', - headers: {}, - }, - }, - ], - [ - { - input: new URL('https://example.com/'), - init: undefined, - expectedUrl: 'https://example.com/', - expectedOptions: { - method: 'GET', - headers: {}, - }, - }, - ], - [ - { - // @ts-ignore - input: new Request('https://example.com'), - init: undefined, - expectedUrl: 'https://example.com/', - expectedOptions: { - method: 'GET', - headers: {}, - }, - }, - ], - [ - { - // @ts-ignore - input: new Request(new URL('https://example.com')), - init: undefined, - expectedUrl: 'https://example.com/', - expectedOptions: { - method: 'GET', - headers: {}, - }, - }, - ], - [ - { - // @ts-ignore - input: new Request('https://example.com', { - method: 'POST', - headers: { 'content-type': 'application/json' }, - body: JSON.stringify({ data: '12345' }), - }), - init: undefined, - expectedUrl: 'https://example.com/', - expectedOptions: { - method: 'POST', - headers: { 'content-type': 'application/json' }, - body: JSON.stringify({ data: '12345' }), - }, - }, - ], - // Here we will add the init object, making sure it overrides the input values - [ - { - // @ts-ignore - input: new Request('https://example.com', { - method: 'POST', - headers: { 'content-type': 'application/json' }, - body: JSON.stringify({ data: '12345' }), - }), - init: { - method: 'PUT', - headers: { 'content-type': 'application/xml' }, - body: JSON.stringify({ data: '54321' }), - }, - expectedUrl: 'https://example.com/', - expectedOptions: { - method: 'PUT', - headers: { 'content-type': 'application/xml' }, - body: JSON.stringify({ data: '54321' }), - }, - }, - ], - [ - { - input: 'https://example.com', - init: { - method: 'PUT', - headers: { 'content-type': 'application/xml' }, - body: JSON.stringify({ data: '54321' }), - }, - expectedUrl: 'https://example.com', - expectedOptions: { - method: 'PUT', - headers: { 'content-type': 'application/xml' }, - body: JSON.stringify({ data: '54321' }), - }, - }, - ], - [ - { - input: new URL('https://example.com'), - init: { - method: 'PUT', - headers: { 'content-type': 'application/xml' }, - body: JSON.stringify({ data: '54321' }), - }, - expectedUrl: 'https://example.com/', - expectedOptions: { - method: 'PUT', - headers: { 'content-type': 'application/xml' }, - body: JSON.stringify({ data: '54321' }), - }, - }, - ], - // Test different formats for body - [ - { - input: 'https://example.com', - init: { - method: 'PUT', - headers: { 'content-type': 'application/xml' }, - // @ts-ignore - body: new Blob(['Blob contents']), - }, - expectedUrl: 'https://example.com', - expectedOptions: { - method: 'PUT', - headers: { 'content-type': 'application/xml' }, - body: 'Blob contents', - }, - }, - ], - [ - { - input: 'https://example.com', - init: { - method: 'PUT', - headers: { 'content-type': 'application/xml' }, - // Unsupported body type - body: 123, - }, - expectedUrl: 'https://example.com', - expectedOptions: { - method: 'PUT', - headers: { 'content-type': 'application/xml' }, - }, - }, - ], - // TODO: Test FormData body - // TODO: Test ReadableStream body - ])( - 'Test parsing fetch command arguments: %p', - async ({ input, init, expectedUrl, expectedOptions }) => { - // @ts-ignore - const { url, options } = await FetchInstrumentation.parseRequestArguments({ input, init }); - expect(url).toEqual(expectedUrl); - expect(options).toEqual(expectedOptions); - } - ); - - test('Test convertHeadersToKeyValuePairs - Headers input', () => { - // @ts-ignore - const headers = new Headers({ - 'content-type': 'application/json', - 'content-length': '12345', - }); - // @ts-ignore - const parsedHeaders = FetchInstrumentation.convertHeadersToKeyValuePairs(headers); - expect(parsedHeaders).toEqual({ - 'content-type': 'application/json', - 'content-length': '12345', - }); - }); - - test('Test convertHeadersToKeyValuePairs - Record input', () => { - const headers = { - 'content-type': 'application/json', - 'content-length': '12345', - }; - // @ts-ignore - const parsedHeaders = FetchInstrumentation.convertHeadersToKeyValuePairs(headers); - expect(parsedHeaders).toEqual({ - 'content-type': 'application/json', - 'content-length': '12345', - }); - }); - - test('Test convertHeadersToKeyValuePairs - string[][] input', () => { - const headers = [ - ['content-type', 'application/json'], - ['content-length', '12345'], - ]; - // @ts-ignore - const parsedHeaders = FetchInstrumentation.convertHeadersToKeyValuePairs(headers); - expect(parsedHeaders).toEqual({ - 'content-type': 'application/json', - 'content-length': '12345', - }); - }); - test('Test addHeadersToFetchArguments - only input given', () => { const expectedHeaders = { 'content-type': 'application/json', @@ -673,7 +463,7 @@ describe('fetch', () => { const input = 'https://example.com'; const init = undefined; // @ts-ignore - const { input: newInput, init: newInit } = FetchInstrumentation.addHeadersToFetchArguments({ + const { input: newInput, init: newInit } = FetchInstrumentation._addHeadersToFetchArguments({ input, init, options, @@ -706,7 +496,7 @@ describe('fetch', () => { body: JSON.stringify({ data: '54321' }), }; // @ts-ignore - const { input: newInput, init: newInit } = FetchInstrumentation.addHeadersToFetchArguments({ + const { input: newInput, init: newInit } = FetchInstrumentation._addHeadersToFetchArguments({ input, init, options, diff --git a/src/hooks/fetch.ts b/src/hooks/fetch.ts index cbf31e18c..be32b7b5a 100644 --- a/src/hooks/fetch.ts +++ b/src/hooks/fetch.ts @@ -1,42 +1,9 @@ import { BaseHttp, ParseHttpRequestOptions, RequestData, UrlAndRequestOptions } from './baseHttp'; import * as logger from '../logger'; import { getEventEntitySize, safeExecuteAsync } from '../utils'; +import { BaseFetch, FetchArguments, RequestExtenderContext } from './baseFetch'; -interface ResponseData { - headers?: Record; - statusCode?: number; - body?: string; - // The time when all the response data was received, including all the body chunks - receivedTime?: number; - truncated?: boolean; - isNetworkError?: boolean; -} - -interface RequestExtenderContext { - isTracedDisabled?: boolean; - awsRequestId?: string; - transactionId?: string; - requestRandomId?: string; - currentSpan?: any; - requestData?: RequestData; - // @ts-ignore - response?: Response; -} - -interface FetchArguments { - // @ts-ignore - input: RequestInfo | URL; - // @ts-ignore - init?: RequestInit; -} - -type FetchUrlAndRequestOptions = UrlAndRequestOptions & { - options: ParseHttpRequestOptions & { - body?: string; - }; -}; - -export class FetchInstrumentation { +export class FetchInstrumentation extends BaseFetch { /** * Starts the fetch instrumentation by attaching the hooks to the fetch function. * Note: safe to call even if the fetch instrumentation was already started / fetch is not available. @@ -155,7 +122,7 @@ export class FetchInstrumentation { }); const originalArgs: FetchArguments = { input, init }; extenderContext.isTracedDisabled = true; - const { url, options } = await FetchInstrumentation.parseRequestArguments(originalArgs); + const { url, options } = await FetchInstrumentation._parseRequestArguments(originalArgs); const requestTracingData = BaseHttp.onRequestCreated({ options, url, @@ -194,7 +161,7 @@ export class FetchInstrumentation { let modifiedArgs: FetchArguments = { ...originalArgs }; if (addedHeaders) { options.headers = headers; - modifiedArgs = FetchInstrumentation.addHeadersToFetchArguments({ ...modifiedArgs, options }); + modifiedArgs = FetchInstrumentation._addHeadersToFetchArguments({ ...modifiedArgs, options }); } extenderContext.isTracedDisabled = false; @@ -224,7 +191,7 @@ export class FetchInstrumentation { } const clonedResponse = response.clone(); - const responseData = FetchInstrumentation.convertResponseToResponseData(clonedResponse); + const responseData = FetchInstrumentation._convertResponseToResponseData(clonedResponse); const responseDataWriterHandler = BaseHttp.createResponseDataWriterHandler({ transactionId, awsRequestId, @@ -237,172 +204,4 @@ export class FetchInstrumentation { responseDataWriterHandler(['data', bodyText]); responseDataWriterHandler(['end']); } - - /** - * Parses the raw arguments passed to the fetch function and returns the URL and options object. - * @param {FetchArguments} args The raw fetch arguments - * @param {RequestInfo | URL} args.input The first argument (called input / resource) that is passed to the fetch command - * @param {RequestInit} args.init The second argument (called init / options) that is passed to the fetch command - * @returns {UrlAndRequestOptions} Our custom request options object containing the URL and options - * @private - */ - private static async parseRequestArguments({ - input, - init, - }: FetchArguments): Promise { - let url: string = undefined; - const options: ParseHttpRequestOptions & { - body?: string; - } = { - headers: {}, - method: 'GET', - }; - - if (input instanceof URL) { - url = input.toString(); - } else if (typeof input === 'string') { - url = input; - // @ts-ignore - } else if (input instanceof Request) { - url = input.url; - options.method = input.method || 'GET'; - options.headers = FetchInstrumentation.convertHeadersToKeyValuePairs(input.headers); - } - - if (init) { - options.method = init.method || options.method || 'GET'; - options.headers = { - ...options.headers, - ...FetchInstrumentation.convertHeadersToKeyValuePairs(init.headers), - }; - } - - // Read the body from the request object, only if we shouldn't look in the init object - let body: string = undefined; - try { - // @ts-ignore - if (input instanceof Request && input.body && !init?.body) { - body = await input.clone().text(); - } - } catch (e) { - logger.debug('Failed to read body from Request object', e); - } - - // If we didn't get the body from the request object, get it from the init object - if (!body && init?.body) { - try { - // @ts-ignore - const decoder = new TextDecoder(); - // @ts-ignore - if (init.body instanceof ReadableStream) { - const reader = init.body.getReader(); - let result = ''; - // Limiting the number of reads to prevent an infinite read loop - for (let i = 0; i < 10000; i++) { - const { done, value } = await reader.read(); - if (done) { - break; - } - result += decoder.decode(value); - } - body = result; - // @ts-ignore - } else if (init.body instanceof Blob) { - body = await init.body.text(); - } else if (init.body instanceof ArrayBuffer) { - body = decoder.decode(init.body); - } else if (typeof init.body === 'string') { - body = init.body; - } else { - // TODO: Implement FormData support - logger.debug('Unsupported request body type', typeof init.body); - } - } catch (e) { - logger.debug('Failed to read request body from Request object', { - error: e, - bodyObjectType: typeof init.body, - }); - } - } - - if (body) { - options.body = body; - } - - return { url, options }; - } - - /** - * Converts the headers object to a key-value pair object. - * Fetch library uses multiple format to represent headers, this function will convert them all to a key-value pair object. - * @param {[string, string][] | Record | Headers} headers Headers object as used by the fetch library - * @returns {Record} The headers as a key-value pair object - * @private - */ - private static convertHeadersToKeyValuePairs( - // @ts-ignore - headers: [string, string][] | Record | Headers - ): Record { - // @ts-ignore - if (headers instanceof Headers) { - const headersObject: Record = {}; - headers.forEach((value, key) => { - headersObject[key] = value; - }); - return headersObject; - } - if (Array.isArray(headers)) { - const headersObject: Record = {}; - headers.forEach(([key, value]) => { - headersObject[key] = value; - }); - return headersObject; - } - - return headers; - } - - /** - * Adds the headers found in the options object to the fetch arguments, and return the modified arguments. - * The original arguments will not be modified. - * @param {FetchArguments} args The original fetch arguments - * @param {ParseHttpRequestOptions} args.input The first argument (called input / resource) that is passed to the fetch command - * @param {RequestInit} args.init The second argument (called init / options) that is passed to the fetch command - * @param {ParseHttpRequestOptions} args.options Our custom request options object containing the headers to add to the fetch arguments - * @returns {FetchArguments} The modified fetch arguments with the headers added from the args.options object - */ - private static addHeadersToFetchArguments({ - input, - init, - options, - }: FetchArguments & { options: ParseHttpRequestOptions }): FetchArguments { - // The init headers take precedence over the input headers - // @ts-ignore - const newInit: RequestInit = init ? { ...init } : {}; - - if (options.headers) { - const currentHeaders = newInit.headers || {}; - newInit.headers = { ...currentHeaders, ...options.headers }; - } - - return { input, init: newInit }; - } - - /** - * Converts the fetch response object instance to a custom response data object used by the rest of the lumigo tracer codebase. - * @param {Response} response - * @returns {ResponseData} - * @private - */ - // @ts-ignore - private static convertResponseToResponseData(response: Response): ResponseData { - const headers = {}; - response.headers.forEach((value, key) => { - headers[key] = value; - }); - return { - headers, - statusCode: response.status, - }; - } }