Skip to content
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
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