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: Implement handling for gzip compressed responses. #367

Merged
merged 8 commits into from
Feb 14, 2024
1 change: 1 addition & 0 deletions contract-tests/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ app.get('/', (req, res) => {
'migrations',
'event-sampling',
'strongly-typed',
'polling-gzip'
],
});
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as http from 'http';
import * as zlib from 'zlib';

import NodeRequests from '../../src/platform/NodeRequests';

Expand Down Expand Up @@ -57,6 +58,9 @@ describe('given a default instance of NodeRequests', () => {
res.destroy();
resetResolve();
}, 0);
} else if ((req.url?.indexOf('gzip') || -1) >= 0) {
res.setHeader('Content-Encoding', 'gzip');
res.end(zlib.gzipSync(Buffer.from(JSON_RESPONSE, 'utf8')));
} else {
res.end(TEXT_RESPONSE);
}
Expand Down Expand Up @@ -144,4 +148,21 @@ describe('given a default instance of NodeRequests', () => {
await res.json();
}).rejects.toThrow();
});

it('includes accept-encoding header', async () => {
await requests.fetch(`http://localhost:${PORT}/gzip`, { method: 'GET' });
const serverResult = await promise;
expect(serverResult.method).toEqual('GET');
expect(serverResult.headers['accept-encoding']).toEqual('gzip');
});

it('can get compressed json from a response', async () => {
const res = await requests.fetch(`http://localhost:${PORT}/gzip`, { method: 'GET' });
expect(res.headers.get('content-type')).toEqual('text/plain');
const json = await res.json();
expect(json).toEqual({ text: 'value' });
const serverResult = await promise;
expect(serverResult.method).toEqual('GET');
expect(serverResult.body).toEqual('');
});
});
12 changes: 11 additions & 1 deletion packages/sdk/server-node/src/platform/NodeRequests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,12 +110,22 @@ export default class NodeRequests implements platform.Requests {
const isSecure = url.startsWith('https://');
const impl = isSecure ? https : http;

// For get requests we are going to automatically support compressed responses.
// Note this does not affect SSE as the event source is not using this fetch implementation.
const headers =
options.method?.toLowerCase() === 'get'
? {
...options.headers,
'accept-encoding': 'gzip',
}
: options.headers;

return new Promise((resolve, reject) => {
const req = impl.request(
url,
{
timeout: options.timeout,
headers: options.headers,
headers,
method: options.method,
agent: this.agent,
},
Expand Down
42 changes: 28 additions & 14 deletions packages/sdk/server-node/src/platform/NodeResponse.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import * as http from 'http';
import { pipeline, Writable } from 'stream';
import * as zlib from 'zlib';

import { platform } from '@launchdarkly/js-server-sdk-common';

Expand All @@ -7,7 +9,15 @@ import HeaderWrapper from './HeaderWrapper';
export default class NodeResponse implements platform.Response {
incomingMessage: http.IncomingMessage;

body: any[] = [];
chunks: any[] = [];

memoryStream: Writable = new Writable({
decodeStrings: true,
write: (chunk, _enc, next) => {
this.chunks.push(chunk);
next();
},
});

promise: Promise<string>;

Expand All @@ -26,20 +36,24 @@ export default class NodeResponse implements platform.Response {
this.incomingMessage = res;

this.promise = new Promise((resolve, reject) => {
res.on('data', (chunk) => {
this.body.push(chunk);
});

res.on('error', (err) => {
this.rejection = err;
if (this.listened) {
reject(err);
// Called on error or completion of the pipeline.
const pipelineCallback = (err: any) => {
if (err) {
this.rejection = err;
if (this.listened) {
reject(err);
}
}
});

res.on('end', () => {
resolve(Buffer.concat(this.body).toString());
});
return resolve(Buffer.concat(this.chunks).toString());
};
switch (res.headers['content-encoding']) {
case 'gzip':
pipeline(res, zlib.createGunzip(), this.memoryStream, pipelineCallback);
break;
default:
pipeline(res, this.memoryStream, pipelineCallback);
break;
}
});
}

Expand Down
Loading