From 735fa7c2be14df4d614ab1fc4851bd1efa85dbd8 Mon Sep 17 00:00:00 2001 From: Salih Erdal Date: Thu, 1 Aug 2024 23:47:52 +0300 Subject: [PATCH] Add request option to expose HTTP response stream --- lib/HttpClient.ts | 2 +- lib/RestClient.ts | 16 +++++++++-- lib/Util.ts | 23 +++++++++++++++ test/units/resttests.ts | 64 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 102 insertions(+), 3 deletions(-) diff --git a/lib/HttpClient.ts b/lib/HttpClient.ts index 498959c..9cbf037 100644 --- a/lib/HttpClient.ts +++ b/lib/HttpClient.ts @@ -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; diff --git a/lib/RestClient.ts b/lib/RestClient.ts index f846d85..b3540ba 100644 --- a/lib/RestClient.ts +++ b/lib/RestClient.ts @@ -8,7 +8,8 @@ import util = require("./Util"); export interface IRestResponse { statusCode: number, result: T | null, - headers: Object + headers: Object, + responseStream?: NodeJS.ReadableStream } export interface IRequestOptions { @@ -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 @@ -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); diff --git a/lib/Util.ts b/lib/Util.ts index fa3c2d7..35ba5e3 100644 --- a/lib/Util.ts +++ b/lib/Util.ts @@ -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'; @@ -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 @@ -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); +} diff --git a/test/units/resttests.ts b/test/units/resttests.ts index 542ee0f..860930b 100644 --- a/test/units/resttests.ts +++ b/test/units/resttests.ts @@ -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; @@ -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 = await _rest.get('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 = await _rest.get('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('/')