Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for compressing request bodies #111

Merged
merged 1 commit into from
Oct 6, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
dpopp07 marked this conversation as resolved.
Show resolved Hide resolved
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