Skip to content

Commit

Permalink
feat: add support for compressing request bodies (#111)
Browse files Browse the repository at this point in the history
a flag can be passed to or set on the BaseService that will enable gzip compression on the request body data
  • Loading branch information
dpopp07 committed Oct 6, 2020
1 parent 4d2a905 commit 7692d71
Show file tree
Hide file tree
Showing 6 changed files with 285 additions and 6 deletions.
6 changes: 5 additions & 1 deletion auth/utils/read-external-sources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ function filterPropertiesByServiceName(envObj: any, serviceName: string): any {
}
});

// all env variables are parsed as strings, convert disable ssl vars to boolean
// all env variables are parsed as strings, convert boolean vars as needed
if (typeof credentials.disableSsl === 'string') {
credentials.disableSsl = credentials.disableSsl === 'true';
}
Expand All @@ -90,6 +90,10 @@ function filterPropertiesByServiceName(envObj: any, serviceName: string): any {
credentials.authDisableSsl = credentials.authDisableSsl === 'true';
}

if (typeof credentials.enableGzip === 'string') {
credentials.enableGzip = credentials.enableGzip === 'true';
}

return credentials;
}

Expand Down
20 changes: 18 additions & 2 deletions lib/base-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,15 @@ export class BaseService {
}
}

/**
* Turn request body compression on or off.
*
* @param {boolean} setting Will turn it on if 'true', off if 'false'.
*/
public setEnableGzipCompression(setting: boolean): void {
this.requestWrapperInstance.compressRequestData = setting;
}

/**
* Configure the service using external configuration
*
Expand Down Expand Up @@ -205,15 +214,22 @@ export class BaseService {
const properties = readExternalSources(serviceName);

if (properties !== null) {
// the user can define two client-level variables in the credentials file: url and disableSsl
const { url, disableSsl } = properties;
// the user can define the following client-level variables in the credentials file:
// - url
// - disableSsl
// - enableGzip

const { url, disableSsl, enableGzip } = properties;

if (url) {
results.serviceUrl = stripTrailingSlash(url);
}
if (disableSsl === true) {
results.disableSslVerification = disableSsl;
}
if (enableGzip === true) {
results.enableGzipCompression = enableGzip;
}
}

return results;
Expand Down
56 changes: 54 additions & 2 deletions lib/request-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,25 @@ import { AxiosRequestConfig } from 'axios';
import axiosCookieJarSupport from 'axios-cookiejar-support';
import extend = require('extend');
import FormData = require('form-data');
import { OutgoingHttpHeaders } from 'http';
import https = require('https');
import isStream = require('isstream');
import querystring = require('querystring');
import { PassThrough as readableStream } from 'stream';
import zlib = require('zlib');
import { buildRequestFileObject, getMissingParams, isEmptyObject, isFileData, isFileWithMetadata, stripTrailingSlash } from './helper';
import logger from './logger';
import { streamToPromise } from './stream-to-promise';

const isBrowser = typeof window === 'object';
const globalTransactionId = 'x-global-transaction-id';

export class RequestWrapper {
private axiosInstance;
private compressRequestData: boolean;

constructor(axiosOptions?) {
axiosOptions = axiosOptions || {};
this.compressRequestData = Boolean(axiosOptions.enableGzipCompression);

// override several axios defaults
// axios sets the default Content-Type for `post`, `put`, and `patch` operations
Expand Down Expand Up @@ -137,7 +142,7 @@ export class RequestWrapper {
* @returns {ReadableStream|undefined}
* @throws {Error}
*/
public sendRequest(parameters): Promise<any> {
public async sendRequest(parameters): Promise<any> {
const options = extend(true, {}, parameters.defaultOptions, parameters.options);
const { path, body, form, formData, qs, method, serviceUrl } = options;
let { headers, url } = options;
Expand Down Expand Up @@ -205,6 +210,11 @@ export class RequestWrapper {
// accept gzip encoded responses if Accept-Encoding is not already set
headers['Accept-Encoding'] = headers['Accept-Encoding'] || 'gzip';

// compress request body data if enabled
if (this.compressRequestData) {
data = await this.gzipRequestBody(data, headers);
}

const requestParams = {
url,
method,
Expand Down Expand Up @@ -311,6 +321,48 @@ export class RequestWrapper {

return error;
}

private async gzipRequestBody(data: any, headers: OutgoingHttpHeaders): Promise<Buffer|any> {
// skip compression if user has set the encoding header to gzip
const contentSetToGzip =
headers['Content-Encoding'] &&
headers['Content-Encoding'].toString().includes('gzip');

if (!data || contentSetToGzip) {
return data;
}

let reqBuffer: Buffer;

try {
if (isStream(data)) {
reqBuffer = Buffer.from(await streamToPromise(data));
} else if (data.toString && data.toString() !== '[object Object]' && !Array.isArray(data)) {
// this handles pretty much any primitive that isnt a JSON object or array
reqBuffer = Buffer.from(data.toString());
} else {
reqBuffer = Buffer.from(JSON.stringify(data));
}
} catch (err) {
logger.error('Error converting request body to a buffer - data will not be compressed.');
logger.debug(err);
return data;
}

try {
data = zlib.gzipSync(reqBuffer);

// update the headers by reference - only if the data was actually compressed
headers['Content-Encoding'] = 'gzip';
} catch (err) {
// if an exception is caught, `data` will still be in its original form
// we can just proceed with the request uncompressed
logger.error('Error compressing request body - data will not be compressed.');
logger.debug(err);
}

return data;
}
}

/**
Expand Down
18 changes: 18 additions & 0 deletions test/unit/base-service.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,22 @@ describe('Base Service', () => {
expect(testService.baseOptions.serviceUrl).toBe(newUrl);
});

it('should support enabling gzip compression after instantiation', () => {
const testService = new TestService({
authenticator: AUTHENTICATOR,
});

expect(testService.baseOptions.enableGzipCompression).toBeFalsy();

const on = true;
testService.setEnableGzipCompression(on);
expect(testService.requestWrapperInstance.compressRequestData).toBe(on);

const off = false;
testService.setEnableGzipCompression(off);
expect(testService.requestWrapperInstance.compressRequestData).toBe(off);
});

it('should throw an error if an authenticator is not passed in', () => {
expect(() => new TestService()).toThrow();
});
Expand Down Expand Up @@ -374,13 +390,15 @@ describe('Base Service', () => {
readExternalSourcesMock.mockImplementation(() => ({
url: 'abc123.com',
disableSsl: true,
enableGzip: true,
}));

testService.configureService(DEFAULT_NAME);

expect(readExternalSourcesMock).toHaveBeenCalled();
expect(testService.baseOptions.serviceUrl).toEqual('abc123.com');
expect(testService.baseOptions.disableSslVerification).toEqual(true);
expect(testService.baseOptions.enableGzipCompression).toEqual(true);
});

it('configureService method should throw error if service name is not provided', () => {
Expand Down
4 changes: 3 additions & 1 deletion test/unit/read-external-sources.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,12 +132,14 @@ describe('Read External Sources Module', () => {
expect(properties.scope).toBe(SCOPE);
});

it('should convert disableSsl values from string to boolean', () => {
it('should convert certain values from string to boolean', () => {
process.env.TEST_SERVICE_DISABLE_SSL = 'true';
process.env.TEST_SERVICE_AUTH_DISABLE_SSL = 'true';
process.env.TEST_SERVICE_ENABLE_GZIP = 'true';
const properties = readExternalSources(SERVICE_NAME);
expect(typeof properties.disableSsl).toBe('boolean');
expect(typeof properties.authDisableSsl).toBe('boolean');
expect(typeof properties.enableGzip).toBe('boolean');
});
});

Expand Down
Loading

0 comments on commit 7692d71

Please sign in to comment.