Skip to content

Commit a52bee1

Browse files
authored
feat: Implement handling for gzip compressed responses. (#367)
This is working against the test harness and LD.
1 parent 4707dd6 commit a52bee1

File tree

4 files changed

+61
-15
lines changed

4 files changed

+61
-15
lines changed

contract-tests/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ app.get('/', (req, res) => {
3131
'migrations',
3232
'event-sampling',
3333
'strongly-typed',
34+
'polling-gzip'
3435
],
3536
});
3637
});

packages/sdk/server-node/__tests__/platform/NodeRequests.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as http from 'http';
2+
import * as zlib from 'zlib';
23

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

@@ -57,6 +58,9 @@ describe('given a default instance of NodeRequests', () => {
5758
res.destroy();
5859
resetResolve();
5960
}, 0);
61+
} else if ((req.url?.indexOf('gzip') || -1) >= 0) {
62+
res.setHeader('Content-Encoding', 'gzip');
63+
res.end(zlib.gzipSync(Buffer.from(JSON_RESPONSE, 'utf8')));
6064
} else {
6165
res.end(TEXT_RESPONSE);
6266
}
@@ -144,4 +148,21 @@ describe('given a default instance of NodeRequests', () => {
144148
await res.json();
145149
}).rejects.toThrow();
146150
});
151+
152+
it('includes accept-encoding header', async () => {
153+
await requests.fetch(`http://localhost:${PORT}/gzip`, { method: 'GET' });
154+
const serverResult = await promise;
155+
expect(serverResult.method).toEqual('GET');
156+
expect(serverResult.headers['accept-encoding']).toEqual('gzip');
157+
});
158+
159+
it('can get compressed json from a response', async () => {
160+
const res = await requests.fetch(`http://localhost:${PORT}/gzip`, { method: 'GET' });
161+
expect(res.headers.get('content-type')).toEqual('text/plain');
162+
const json = await res.json();
163+
expect(json).toEqual({ text: 'value' });
164+
const serverResult = await promise;
165+
expect(serverResult.method).toEqual('GET');
166+
expect(serverResult.body).toEqual('');
167+
});
147168
});

packages/sdk/server-node/src/platform/NodeRequests.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,12 +110,22 @@ export default class NodeRequests implements platform.Requests {
110110
const isSecure = url.startsWith('https://');
111111
const impl = isSecure ? https : http;
112112

113+
// For get requests we are going to automatically support compressed responses.
114+
// Note this does not affect SSE as the event source is not using this fetch implementation.
115+
const headers =
116+
options.method?.toLowerCase() === 'get'
117+
? {
118+
...options.headers,
119+
'accept-encoding': 'gzip',
120+
}
121+
: options.headers;
122+
113123
return new Promise((resolve, reject) => {
114124
const req = impl.request(
115125
url,
116126
{
117127
timeout: options.timeout,
118-
headers: options.headers,
128+
headers,
119129
method: options.method,
120130
agent: this.agent,
121131
},

packages/sdk/server-node/src/platform/NodeResponse.ts

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import * as http from 'http';
2+
import { pipeline, Writable } from 'stream';
3+
import * as zlib from 'zlib';
24

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

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

10-
body: any[] = [];
12+
chunks: any[] = [];
13+
14+
memoryStream: Writable = new Writable({
15+
decodeStrings: true,
16+
write: (chunk, _enc, next) => {
17+
this.chunks.push(chunk);
18+
next();
19+
},
20+
});
1121

1222
promise: Promise<string>;
1323

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

2838
this.promise = new Promise((resolve, reject) => {
29-
res.on('data', (chunk) => {
30-
this.body.push(chunk);
31-
});
32-
33-
res.on('error', (err) => {
34-
this.rejection = err;
35-
if (this.listened) {
36-
reject(err);
39+
// Called on error or completion of the pipeline.
40+
const pipelineCallback = (err: any) => {
41+
if (err) {
42+
this.rejection = err;
43+
if (this.listened) {
44+
reject(err);
45+
}
3746
}
38-
});
39-
40-
res.on('end', () => {
41-
resolve(Buffer.concat(this.body).toString());
42-
});
47+
return resolve(Buffer.concat(this.chunks).toString());
48+
};
49+
switch (res.headers['content-encoding']) {
50+
case 'gzip':
51+
pipeline(res, zlib.createGunzip(), this.memoryStream, pipelineCallback);
52+
break;
53+
default:
54+
pipeline(res, this.memoryStream, pipelineCallback);
55+
break;
56+
}
4357
});
4458
}
4559

0 commit comments

Comments
 (0)