Skip to content

Commit

Permalink
feat: Implement handling for gzip compressed responses. (#367)
Browse files Browse the repository at this point in the history
This is working against the test harness and LD.
  • Loading branch information
kinyoklion authored Feb 14, 2024
1 parent 4707dd6 commit a52bee1
Show file tree
Hide file tree
Showing 4 changed files with 61 additions and 15 deletions.
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
21 changes: 21 additions & 0 deletions packages/sdk/server-node/__tests__/platform/NodeRequests.test.ts
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

0 comments on commit a52bee1

Please sign in to comment.