Skip to content

Commit bf899ab

Browse files
authored
Preventing deep copying the request when making http calls (#412)
* Preventing unnecessary copying of the entire request when making http calls * Preventing copying at AuthorizedHttpClient
1 parent 3c3e415 commit bf899ab

File tree

2 files changed

+80
-18
lines changed

2 files changed

+80
-18
lines changed

src/utils/api-request.ts

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -180,11 +180,10 @@ export class HttpClient {
180180
* Sends an HTTP request based on the provided configuration. This is a wrapper around the http and https
181181
* packages of Node.js, providing content processing, timeouts and error handling.
182182
*/
183-
function sendRequest(httpRequestConfig: HttpRequestConfig): Promise<LowLevelResponse> {
184-
const config: HttpRequestConfig = deepCopy(httpRequestConfig);
183+
function sendRequest(config: HttpRequestConfig): Promise<LowLevelResponse> {
185184
return new Promise((resolve, reject) => {
186185
let data: Buffer;
187-
const headers = config.headers || {};
186+
const headers = Object.assign({}, config.headers);
188187
let fullUrl: string = config.url;
189188
if (config.data) {
190189
// GET and HEAD do not support body in request.
@@ -197,15 +196,14 @@ function sendRequest(httpRequestConfig: HttpRequestConfig): Promise<LowLevelResp
197196
}
198197

199198
// Parse URL and append data to query string.
200-
const configUrl = new url.URL(fullUrl);
201-
for (const key in config.data as any) {
202-
if (config.data.hasOwnProperty(key)) {
203-
configUrl.searchParams.append(
204-
key,
205-
(config.data as {[key: string]: string})[key]);
199+
const parsedUrl = new url.URL(fullUrl);
200+
const dataObj = config.data as {[key: string]: string};
201+
for (const key in dataObj) {
202+
if (dataObj.hasOwnProperty(key)) {
203+
parsedUrl.searchParams.append(key, dataObj[key]);
206204
}
207205
}
208-
fullUrl = configUrl.toString();
206+
fullUrl = parsedUrl.toString();
209207
} else if (validator.isObject(config.data)) {
210208
data = Buffer.from(JSON.stringify(config.data), 'utf-8');
211209
if (typeof headers['Content-Type'] === 'undefined') {
@@ -362,8 +360,8 @@ export class AuthorizedHttpClient extends HttpClient {
362360

363361
public send(request: HttpRequestConfig): Promise<HttpResponse> {
364362
return this.app.INTERNAL.getToken().then((accessTokenObj) => {
365-
const requestCopy = deepCopy(request);
366-
requestCopy.headers = requestCopy.headers || {};
363+
const requestCopy = Object.assign({}, request);
364+
requestCopy.headers = Object.assign({}, request.headers);
367365
const authHeader = 'Authorization';
368366
requestCopy.headers[authHeader] = `Bearer ${accessTokenObj.accessToken}`;
369367

test/unit/utils/api-request.spec.ts

Lines changed: 70 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
'use strict';
1818

19-
import * as _ from 'lodash';
2019
import * as chai from 'chai';
2120
import * as nock from 'nock';
2221
import * as sinon from 'sinon';
@@ -28,8 +27,9 @@ import * as mocks from '../../resources/mocks';
2827

2928
import {FirebaseApp} from '../../../src/firebase-app';
3029
import {
31-
ApiSettings, HttpClient, HttpError, AuthorizedHttpClient, ApiCallbackFunction,
30+
ApiSettings, HttpClient, HttpError, AuthorizedHttpClient, ApiCallbackFunction, HttpRequestConfig,
3231
} from '../../../src/utils/api-request';
32+
import { deepCopy } from '../../../src/utils/deep-copy';
3333
import {Agent} from 'http';
3434

3535
chai.should();
@@ -95,7 +95,7 @@ describe('HttpClient', () => {
9595
let transportSpy: sinon.SinonSpy = null;
9696

9797
afterEach(() => {
98-
_.forEach(mockedRequests, (mockedRequest) => mockedRequest.done());
98+
mockedRequests.forEach((mockedRequest) => mockedRequest.done());
9999
mockedRequests = [];
100100
if (transportSpy) {
101101
transportSpy.restore();
@@ -202,6 +202,38 @@ describe('HttpClient', () => {
202202
});
203203
});
204204

205+
it('should not mutate the arguments', () => {
206+
const reqData = {request: 'data'};
207+
const scope = nock('https://' + mockHost, {
208+
reqheaders: {
209+
'Authorization': 'Bearer token',
210+
'Content-Type': (header) => {
211+
return header.startsWith('application/json'); // auto-inserted
212+
},
213+
'My-Custom-Header': 'CustomValue',
214+
},
215+
}).post(mockPath, reqData)
216+
.reply(200, {success: true}, {
217+
'content-type': 'application/json',
218+
});
219+
mockedRequests.push(scope);
220+
const client = new HttpClient();
221+
const request: HttpRequestConfig = {
222+
method: 'POST',
223+
url: mockUrl,
224+
headers: {
225+
'authorization': 'Bearer token',
226+
'My-Custom-Header': 'CustomValue',
227+
},
228+
data: reqData,
229+
};
230+
const requestCopy = deepCopy(request);
231+
return client.send(request).then((resp) => {
232+
expect(resp.status).to.equal(200);
233+
expect(request).to.deep.equal(requestCopy);
234+
});
235+
});
236+
205237
it('should make a GET request with the provided headers and data', () => {
206238
const reqData = {key1: 'value1', key2: 'value2'};
207239
const respData = {success: true};
@@ -421,7 +453,7 @@ describe('AuthorizedHttpClient', () => {
421453
});
422454

423455
afterEach(() => {
424-
_.forEach(mockedRequests, (mockedRequest) => mockedRequest.done());
456+
mockedRequests.forEach((mockedRequest) => mockedRequest.done());
425457
mockedRequests = [];
426458
return mockApp.delete();
427459
});
@@ -516,9 +548,8 @@ describe('AuthorizedHttpClient', () => {
516548
const respData = {success: true};
517549
const options = {
518550
reqheaders: {
519-
'Authorization': 'Bearer token',
520551
'Content-Type': (header: string) => {
521-
return header.startsWith('application/json'); // auto-inserted by Axios
552+
return header.startsWith('application/json'); // auto-inserted
522553
},
523554
'My-Custom-Header': 'CustomValue',
524555
},
@@ -544,6 +575,39 @@ describe('AuthorizedHttpClient', () => {
544575
expect(resp.data).to.deep.equal(respData);
545576
});
546577
});
578+
579+
it('should not mutate the arguments', () => {
580+
const reqData = {request: 'data'};
581+
const options = {
582+
reqheaders: {
583+
'Content-Type': (header: string) => {
584+
return header.startsWith('application/json'); // auto-inserted
585+
},
586+
'My-Custom-Header': 'CustomValue',
587+
},
588+
};
589+
Object.assign(options.reqheaders, requestHeaders.reqheaders);
590+
const scope = nock('https://' + mockHost, options)
591+
.post(mockPath, reqData)
592+
.reply(200, {success: true}, {
593+
'content-type': 'application/json',
594+
});
595+
mockedRequests.push(scope);
596+
const client = new AuthorizedHttpClient(mockApp);
597+
const request: HttpRequestConfig = {
598+
method: 'POST',
599+
url: mockUrl,
600+
headers: {
601+
'My-Custom-Header': 'CustomValue',
602+
},
603+
data: reqData,
604+
};
605+
const requestCopy = deepCopy(request);
606+
return client.send(request).then((resp) => {
607+
expect(resp.status).to.equal(200);
608+
expect(request).to.deep.equal(requestCopy);
609+
});
610+
});
547611
});
548612

549613
describe('ApiSettings', () => {

0 commit comments

Comments
 (0)