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

Add request option to expose HTTP response stream #376

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion lib/HttpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export class HttpClientResponse implements ifm.IHttpClientResponse {
// Extract Encoding from header: 'content-encoding'
// Match `gzip`, `gzip, deflate` variations of GZIP encoding
const contentEncoding: string = this.message.headers['content-encoding'] || '';
const isGzippedEncoded: boolean = new RegExp('(gzip$)|(gzip, *deflate)').test(contentEncoding);
const isGzippedEncoded: boolean = util.isGzippedEncoded(contentEncoding);

this.message.on('data', function(data: string|Buffer) {
const chunk = (typeof data === 'string') ? Buffer.from(data, encodingCharset) : data;
Expand Down
16 changes: 14 additions & 2 deletions lib/RestClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import util = require("./Util");
export interface IRestResponse<T> {
statusCode: number,
result: T | null,
headers: Object
headers: Object,
responseStream?: NodeJS.ReadableStream
}

export interface IRequestOptions {
Expand All @@ -19,6 +20,7 @@ export interface IRequestOptions {
additionalHeaders?: ifm.IHeaders,

responseProcessor?: Function,
responseAsStream?: boolean,
//Dates aren't automatically deserialized by JSON, this adds a date reviver to ensure they aren't just left as strings
deserializeDates?: boolean,
queryParameters?: ifm.IRequestQueryParams
Expand Down Expand Up @@ -215,7 +217,17 @@ export class RestClient {

// get the result from the body
try {
contents = await res.readBody();
if (options?.responseAsStream) {
const contentEncoding: string = res.message.headers['content-encoding'] || '';
const isGzippedEncoded: boolean = util.isGzippedEncoded(contentEncoding);
if (isGzippedEncoded) {
response.responseStream = util.gunzippedBodyStream(res.message);
} else {
response.responseStream = res.message;
}
} else {
contents = await res.readBody();
}
if (contents && contents.length > 0) {
if (options && options.deserializeDates) {
obj = JSON.parse(contents, RestClient.dateTimeDeserializer);
Expand Down
23 changes: 23 additions & 0 deletions lib/Util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import * as qs from 'qs';
import * as url from 'url';
import * as path from 'path';
import http = require('http');
import zlib = require('zlib');
import { IRequestQueryParams, IHttpClientResponse } from './Interfaces';

Expand Down Expand Up @@ -98,6 +99,18 @@ export async function decompressGzippedContent(buffer: Buffer, charset?: BufferE
})
}

/**
* Pipe a http response stream through a gunzip stream
*
* @param message - http response stream
* @returns response stream piped through gunzip
*/
export function gunzippedBodyStream(message: http.IncomingMessage) {
const gunzip = zlib.createGunzip();
message.pipe(gunzip);
return gunzip;
}

/**
* Builds a RegExp to test urls against for deciding
* wether to bypass proxy from an entry of the
Expand Down Expand Up @@ -144,3 +157,13 @@ export function obtainContentCharset (response: IHttpClientResponse) : BufferEnc

return 'utf-8';
}

/**
* Test if the content encoding string matches gzip or deflate
*
* @param {string} contentEncoding
* @returns {boolean}
*/
export function isGzippedEncoded(contentEncoding: string): boolean {
return new RegExp('(gzip$)|(gzip, *deflate)').test(contentEncoding);
}
64 changes: 64 additions & 0 deletions test/units/resttests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import nock = require('nock');
import * as ifm from 'typed-rest-client/Interfaces';
import * as restm from 'typed-rest-client/RestClient';
import * as util from 'typed-rest-client/Util';
import {Readable} from 'stream';
import {gzipSync} from 'zlib';

export interface HttpData {
url: string;
Expand Down Expand Up @@ -116,6 +118,68 @@ describe('Rest Tests', function () {
assert.equal(restRes.result.json.nonDateProperty, 'stringObject');
});

it('gets a resource and exposes its response stream', async() => {
//Arrange
nock('http://microsoft.com')
.get('/file')
.reply(200, () => {
return Readable.from(Buffer.from('test', 'utf-8'));
});

//Act
const restRes: restm.IRestResponse<HttpData> = await _rest.get<HttpData>('http://microsoft.com/file', {responseAsStream: true});
//Assert
assert(restRes.responseStream);
assert(restRes.statusCode == 200, "statusCode should be 200");
assert(restRes.responseStream instanceof Readable);
try {
const data = await new Promise((resolve, reject) => {
let data = '';
restRes.responseStream.on('data', (chunk) => {
data += chunk;
});
restRes.responseStream.on('end', () => resolve(data));
restRes.responseStream.on('error', (err) => reject(err));
});
assert.equal(data, 'test');
} catch (err) {
assert(false, 'should not throw');
}
});

it('gets a resource and exposes its gunzipped response stream', async() => {
//Arrange
nock('http://microsoft.com')
.get('/file')
.reply(200, () => {
const gzipData = gzipSync(Buffer.from('test', 'utf-8'));
return Readable.from(gzipData);
}, {
'Content-Encoding': 'gzip'
});

//Act
const restRes: restm.IRestResponse<HttpData> = await _rest.get<HttpData>('http://microsoft.com/file', {responseAsStream: true});

//Assert
assert(restRes.responseStream);
assert(restRes.statusCode == 200, "statusCode should be 200");
assert(restRes.responseStream instanceof Readable);
try {
const data = await new Promise((resolve, reject) => {
let data = '';
restRes.responseStream.on('data', (chunk) => {
data += chunk;
});
restRes.responseStream.on('end', () => resolve(data));
restRes.responseStream.on('error', (err) => reject(err));
});
assert.equal(data, 'test');
} catch (err) {
assert(false, 'should not throw');
}
});

it('creates a resource', async() => {
nock('http://microsoft.com')
.post('/')
Expand Down