webhoster 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +74 -58
- package/.github/copilot-instructions.md +100 -0
- package/.github/workflows/test-matrix.yml +37 -0
- package/.test/benchmark.js +28 -0
- package/.test/constants.js +4 -0
- package/{test → .test}/http2server.js +1 -1
- package/{test → .test}/httpserver.js +1 -1
- package/{test → .test}/index.js +178 -192
- package/.test/multipromise.js +32 -0
- package/{test → .test}/tls.js +3 -3
- package/{test → .test}/urlencoded.js +3 -0
- package/.vscode/launch.json +24 -3
- package/README.md +116 -90
- package/data/CookieObject.js +14 -14
- package/errata/socketio.js +6 -11
- package/examples/starter.js +11 -0
- package/helpers/HeadersParser.js +7 -8
- package/helpers/HttpListener.js +387 -42
- package/helpers/RequestHeaders.js +43 -36
- package/helpers/RequestReader.js +27 -26
- package/helpers/ResponseHeaders.js +47 -36
- package/jsconfig.json +1 -1
- package/lib/HttpHandler.js +447 -277
- package/lib/HttpRequest.js +383 -39
- package/lib/HttpResponse.js +316 -52
- package/lib/HttpTransaction.js +146 -0
- package/middleware/AutoHeadersMiddleware.js +73 -0
- package/middleware/CORSMiddleware.js +45 -47
- package/middleware/CaseInsensitiveHeadersMiddleware.js +5 -11
- package/middleware/ContentDecoderMiddleware.js +81 -35
- package/middleware/ContentEncoderMiddleware.js +179 -132
- package/middleware/ContentLengthMiddleware.js +66 -41
- package/middleware/ContentWriterMiddleware.js +5 -5
- package/middleware/HashMiddleware.js +68 -40
- package/middleware/HeadMethodMiddleware.js +24 -21
- package/middleware/MethodMiddleware.js +29 -36
- package/middleware/PathMiddleware.js +49 -66
- package/middleware/ReadFormData.js +99 -0
- package/middleware/SendJsonMiddleware.js +131 -0
- package/middleware/SendStringMiddleware.js +87 -0
- package/package.json +38 -29
- package/polyfill/FormData.js +164 -0
- package/rollup.config.js +0 -1
- package/scripts/test-all-sync.sh +6 -0
- package/scripts/test-all.sh +7 -0
- package/templates/starter.js +53 -0
- package/test/fixtures/stream.js +68 -0
- package/test/helpers/HttpListener/construct.js +18 -0
- package/test/helpers/HttpListener/customOptions.js +22 -0
- package/test/helpers/HttpListener/doubleCreate.js +40 -0
- package/test/helpers/HttpListener/events.js +77 -0
- package/test/helpers/HttpListener/http.js +31 -0
- package/test/helpers/HttpListener/http2.js +41 -0
- package/test/helpers/HttpListener/https.js +38 -0
- package/test/helpers/HttpListener/startAll.js +31 -0
- package/test/helpers/HttpListener/stopNotStarted.js +23 -0
- package/test/lib/HttpHandler/class.js +8 -0
- package/test/lib/HttpHandler/handleRequest.js +11 -0
- package/test/lib/HttpHandler/middleware.js +941 -0
- package/test/lib/HttpHandler/parse.js +41 -0
- package/test/lib/HttpRequest/class.js +8 -0
- package/test/lib/HttpRequest/downstream.js +171 -0
- package/test/lib/HttpRequest/properties.js +101 -0
- package/test/lib/HttpRequest/read.js +518 -0
- package/test/lib/HttpResponse/class.js +8 -0
- package/test/lib/HttpResponse/properties.js +59 -0
- package/test/lib/HttpResponse/send.js +275 -0
- package/test/lib/HttpTransaction/class.js +8 -0
- package/test/lib/HttpTransaction/ping.js +50 -0
- package/test/lib/HttpTransaction/push.js +89 -0
- package/test/middleware/SendJsonMiddleware.js +222 -0
- package/test/sanity.js +10 -0
- package/test/templates/starter.js +93 -0
- package/tsconfig.json +12 -0
- package/types/index.js +61 -34
- package/types/typings.d.ts +8 -9
- package/utils/AsyncObject.js +6 -3
- package/utils/CaseInsensitiveObject.js +2 -3
- package/utils/function.js +1 -7
- package/utils/headers.js +42 -0
- package/utils/qualityValues.js +1 -1
- package/utils/stream.js +4 -20
- package/index.cjs +0 -3190
- package/test/constants.js +0 -4
- /package/{test → .test}/cookietester.js +0 -0
|
@@ -1,44 +1,56 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { promisify } from 'node:util';
|
|
2
|
+
import {
|
|
3
|
+
// @ts-expect-error Bad types
|
|
4
|
+
BrotliCompress, Deflate, Gzip,
|
|
5
|
+
constants as ZlibContants,
|
|
6
|
+
brotliCompress, brotliCompressSync,
|
|
7
|
+
deflate, deflateSync,
|
|
8
|
+
gzip, gzipSync,
|
|
9
|
+
} from 'node:zlib';
|
|
3
10
|
|
|
4
11
|
import { parseQualityValues } from '../utils/qualityValues.js';
|
|
5
12
|
|
|
6
|
-
|
|
13
|
+
const { BROTLI_OPERATION_FLUSH, Z_SYNC_FLUSH } = ZlibContants;
|
|
14
|
+
|
|
15
|
+
/** @typedef {import('http').IncomingHttpHeaders} IncomingHttpHeaders */
|
|
16
|
+
/** @typedef {import('../lib/HttpRequest.js').default} HttpRequest */
|
|
17
|
+
/** @typedef {import('../lib/HttpResponse.js').default} HttpResponse */
|
|
7
18
|
/** @typedef {import('../types').MiddlewareFunction} MiddlewareFunction */
|
|
8
|
-
/** @typedef {import('../types').
|
|
9
|
-
/** @typedef {import('../types').MiddlewareFunctionResult} MiddlewareFunctionResult */
|
|
19
|
+
/** @typedef {import('../types').ResponseFinalizer} ResponseFinalizer */
|
|
10
20
|
|
|
11
21
|
/** @typedef {'br'|'gzip'|'deflate'|'identity'|'*'} COMPATIBLE_ENCODING */
|
|
12
22
|
|
|
13
23
|
const DEFAULT_MINIMUM_SIZE = 256;
|
|
14
24
|
|
|
25
|
+
const DEFAULT_ASYNC_THRESHOLD = 64 * 1024;
|
|
26
|
+
|
|
15
27
|
/**
|
|
16
28
|
* @typedef ContentEncoderMiddlewareOptions
|
|
17
29
|
* @prop {number} [chunkSize]
|
|
18
30
|
* @prop {boolean} [respondNotAcceptable=false]
|
|
19
31
|
* @prop {'br'|'gzip'|'deflate'|'identity'} [preferredEncoding='identity']
|
|
32
|
+
* Minimum content size before using any compression
|
|
20
33
|
* @prop {number} [minimumSize=DEFAULT_MINIMUM_SIZE]
|
|
34
|
+
* Minimum content size before using async compression
|
|
35
|
+
* @prop {number} [asyncThreshold=DEFAULT_ASYNC_THRESHOLD]
|
|
21
36
|
*/
|
|
22
37
|
|
|
23
38
|
/** @type {COMPATIBLE_ENCODING[]} */
|
|
24
39
|
const COMPATIBLE_ENCODINGS = ['br', 'gzip', 'deflate', 'identity', '*'];
|
|
25
40
|
|
|
26
|
-
/** @implements {IMiddleware} */
|
|
27
41
|
export default class ContentEncoderMiddleware {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
this.minimumSize = options.minimumSize ?? DEFAULT_MINIMUM_SIZE;
|
|
34
|
-
}
|
|
42
|
+
static BrotliCompressAsync = promisify(brotliCompress);
|
|
43
|
+
|
|
44
|
+
static GzipAsync = promisify(gzip);
|
|
45
|
+
|
|
46
|
+
static DeflateAsync = promisify(deflate);
|
|
35
47
|
|
|
36
48
|
/**
|
|
37
|
-
* @param {
|
|
49
|
+
* @param {IncomingHttpHeaders} headers
|
|
38
50
|
* @throws {NotAcceptableException} Error with `NOT_ACCEPTIBLE` message
|
|
39
51
|
* @return {COMPATIBLE_ENCODING}
|
|
40
52
|
*/
|
|
41
|
-
static chooseEncoding(
|
|
53
|
+
static chooseEncoding(headers) {
|
|
42
54
|
/**
|
|
43
55
|
* A request without an Accept-Encoding header field implies that the
|
|
44
56
|
* user agent has no preferences regarding content-codings. Although
|
|
@@ -46,12 +58,11 @@ export default class ContentEncoderMiddleware {
|
|
|
46
58
|
* does not imply that the user agent will be able to correctly process
|
|
47
59
|
* all encodings.
|
|
48
60
|
*/
|
|
49
|
-
if ('accept-encoding' in
|
|
61
|
+
if ('accept-encoding' in headers === false) {
|
|
50
62
|
return '*';
|
|
51
63
|
}
|
|
52
64
|
|
|
53
|
-
/** @type {string} */
|
|
54
|
-
const acceptString = (req.headers['accept-encoding']);
|
|
65
|
+
const acceptString = /** @type {string} */ (headers['accept-encoding']);
|
|
55
66
|
const encodings = parseQualityValues(acceptString?.toLowerCase());
|
|
56
67
|
if (!encodings.size) {
|
|
57
68
|
/**
|
|
@@ -64,18 +75,18 @@ export default class ContentEncoderMiddleware {
|
|
|
64
75
|
let encoding = COMPATIBLE_ENCODINGS[0];
|
|
65
76
|
const allowWildcards = (encodings.get('*')?.q !== 0);
|
|
66
77
|
const encodingEntries = [...encodings.entries()];
|
|
67
|
-
// @ts-
|
|
78
|
+
// @ts-expect-error Cannot cast to COMPATIBLE_ENCODINGS
|
|
68
79
|
encoding = (encodingEntries.find(([value, spec]) => spec.q !== 0 && COMPATIBLE_ENCODINGS.includes(value))?.[0]);
|
|
69
80
|
if (allowWildcards && (encoding === '*' || !encoding)) {
|
|
70
|
-
|
|
71
|
-
|
|
81
|
+
// Server preference
|
|
82
|
+
// Get first compatible encoding not specified
|
|
72
83
|
encoding = COMPATIBLE_ENCODINGS.find((value) => !encodings.has(value));
|
|
73
84
|
}
|
|
74
85
|
if (allowWildcards && !encoding) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
encoding = encodingEntries
|
|
78
|
-
// @ts-
|
|
86
|
+
// Get highest q'd compatible encoding not q=0 or '*'
|
|
87
|
+
// @ts-expect-error Cannot cast to COMPATIBLE_ENCODINGS
|
|
88
|
+
encoding = /** @type {COMPATIBLE_ENCODINGS} */ encodingEntries
|
|
89
|
+
// @ts-expect-error Cannot cast to COMPATIBLE_ENCODINGS
|
|
79
90
|
.find(([value, spec]) => spec.q !== 0 && value !== '*' && COMPATIBLE_ENCODINGS.includes(value))?.[0];
|
|
80
91
|
}
|
|
81
92
|
if (!encoding) {
|
|
@@ -84,28 +95,32 @@ export default class ContentEncoderMiddleware {
|
|
|
84
95
|
return encoding;
|
|
85
96
|
}
|
|
86
97
|
|
|
98
|
+
/** @param {ContentEncoderMiddlewareOptions} [options] */
|
|
99
|
+
constructor(options = {}) {
|
|
100
|
+
this.chunkSize = options.chunkSize;
|
|
101
|
+
this.respondNotAcceptable = options.respondNotAcceptable === true;
|
|
102
|
+
this.preferredEncoding = options.preferredEncoding ?? 'identity';
|
|
103
|
+
this.minimumSize = options.minimumSize ?? DEFAULT_MINIMUM_SIZE;
|
|
104
|
+
this.asyncThreshold = options.asyncThreshold ?? DEFAULT_ASYNC_THRESHOLD;
|
|
105
|
+
this.finalizeResponse = this.finalizeResponse.bind(this);
|
|
106
|
+
}
|
|
107
|
+
|
|
87
108
|
/**
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* @param {MiddlewareFunctionParams} params
|
|
91
|
-
* @return {MiddlewareFunctionResult}
|
|
109
|
+
* @param {HttpResponse} response
|
|
110
|
+
* @return {void}
|
|
92
111
|
*/
|
|
93
|
-
|
|
94
|
-
if (req.method === 'HEAD') {
|
|
95
|
-
// Never needs content-encoding
|
|
96
|
-
return 'continue';
|
|
97
|
-
}
|
|
98
|
-
|
|
112
|
+
addTransformStream(response) {
|
|
99
113
|
/** @type {COMPATIBLE_ENCODING} */
|
|
100
114
|
let parsedEncoding;
|
|
101
115
|
if (this.respondNotAcceptable) {
|
|
102
|
-
|
|
116
|
+
// Parse now to catch the error;
|
|
103
117
|
try {
|
|
104
|
-
parsedEncoding = ContentEncoderMiddleware.chooseEncoding(
|
|
118
|
+
parsedEncoding = ContentEncoderMiddleware.chooseEncoding(response.request.headers);
|
|
105
119
|
} catch (error) {
|
|
106
120
|
if (error?.message === 'NOT_ACCEPTABLE') {
|
|
107
|
-
|
|
108
|
-
|
|
121
|
+
response.status = 406;
|
|
122
|
+
response.end();
|
|
123
|
+
throw new Error('NOT_ACCEPTABLE');
|
|
109
124
|
}
|
|
110
125
|
// Unknown error
|
|
111
126
|
throw error;
|
|
@@ -116,7 +131,7 @@ export default class ContentEncoderMiddleware {
|
|
|
116
131
|
const getContentEncoding = () => {
|
|
117
132
|
if (!parsedEncoding) {
|
|
118
133
|
try {
|
|
119
|
-
parsedEncoding = ContentEncoderMiddleware.chooseEncoding(
|
|
134
|
+
parsedEncoding = ContentEncoderMiddleware.chooseEncoding(response.request.headers);
|
|
120
135
|
} catch (error) {
|
|
121
136
|
if (error?.message !== 'NOT_ACCEPTABLE') {
|
|
122
137
|
throw error;
|
|
@@ -126,118 +141,150 @@ export default class ContentEncoderMiddleware {
|
|
|
126
141
|
if (!parsedEncoding || parsedEncoding === '*') {
|
|
127
142
|
parsedEncoding = this.preferredEncoding || 'identity';
|
|
128
143
|
}
|
|
129
|
-
|
|
144
|
+
response.headers['content-encoding'] = parsedEncoding;
|
|
130
145
|
return parsedEncoding;
|
|
131
146
|
};
|
|
132
147
|
|
|
133
|
-
let
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
148
|
+
let encoding = response.request.headers['content-encoding'];
|
|
149
|
+
// Only continue if unset (missing header). Blank is still considered set.
|
|
150
|
+
// This allows forced encoding (eg: use gzip regardless of size; always identity)
|
|
151
|
+
|
|
152
|
+
// Unset means server preference
|
|
153
|
+
if (encoding == null) {
|
|
154
|
+
encoding = getContentEncoding().toLowerCase?.();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const isEventStream = response.headers['content-type']?.includes('text/event-stream');
|
|
158
|
+
|
|
159
|
+
let newStream;
|
|
160
|
+
switch (encoding) {
|
|
161
|
+
case 'br':
|
|
162
|
+
// @ts-expect-error Bad types
|
|
163
|
+
newStream = new BrotliCompress({
|
|
164
|
+
chunkSize: this.chunkSize,
|
|
165
|
+
flush: isEventStream ? BROTLI_OPERATION_FLUSH : undefined,
|
|
143
166
|
});
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
167
|
+
break;
|
|
168
|
+
case 'gzip':
|
|
169
|
+
// @ts-expect-error Bad types
|
|
170
|
+
newStream = new Gzip({
|
|
171
|
+
chunkSize: this.chunkSize,
|
|
172
|
+
flush: isEventStream ? Z_SYNC_FLUSH : undefined,
|
|
173
|
+
});
|
|
174
|
+
break;
|
|
175
|
+
case 'deflate':
|
|
176
|
+
// @ts-expect-error Bad types
|
|
177
|
+
newStream = new Deflate({
|
|
178
|
+
chunkSize: this.chunkSize,
|
|
179
|
+
flush: isEventStream ? Z_SYNC_FLUSH : undefined,
|
|
180
|
+
});
|
|
181
|
+
break;
|
|
182
|
+
default:
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
response.pipes.push(newStream);
|
|
186
|
+
}
|
|
152
187
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
let gzipStream;
|
|
160
|
-
switch (encoding) {
|
|
161
|
-
case 'deflate':
|
|
162
|
-
gzipStream = createDeflate({ chunkSize: this.chunkSize });
|
|
163
|
-
break;
|
|
164
|
-
case 'gzip':
|
|
165
|
-
gzipStream = createGzip({ chunkSize: this.chunkSize });
|
|
166
|
-
break;
|
|
167
|
-
case 'br':
|
|
168
|
-
gzipStream = createBrotliCompress({ chunkSize: this.chunkSize });
|
|
169
|
-
break;
|
|
170
|
-
default:
|
|
171
|
-
throw new Error('UNKNOWN_ENCODING');
|
|
172
|
-
}
|
|
188
|
+
/** @type {ResponseFinalizer} */
|
|
189
|
+
finalizeResponse(response) {
|
|
190
|
+
if (response.isStreaming) {
|
|
191
|
+
this.addTransformStream(response);
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
173
194
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
});
|
|
189
|
-
gzipStream.on('end', () => {
|
|
190
|
-
let chunk;
|
|
191
|
-
// eslint-disable-next-line no-cond-assign
|
|
192
|
-
while (chunk = pendingChunks.shift()) {
|
|
193
|
-
destination.write(chunk);
|
|
195
|
+
if (response.body == null) return true;
|
|
196
|
+
|
|
197
|
+
/** @type {COMPATIBLE_ENCODING} */
|
|
198
|
+
let parsedEncoding;
|
|
199
|
+
if (this.respondNotAcceptable) {
|
|
200
|
+
// Parse now to catch the error;
|
|
201
|
+
try {
|
|
202
|
+
parsedEncoding = ContentEncoderMiddleware.chooseEncoding(response.request.headers);
|
|
203
|
+
} catch (error) {
|
|
204
|
+
if (error?.message === 'NOT_ACCEPTABLE') {
|
|
205
|
+
// Strip content
|
|
206
|
+
response.body = null;
|
|
207
|
+
response.status = 206;
|
|
208
|
+
return false;
|
|
194
209
|
}
|
|
195
|
-
|
|
196
|
-
|
|
210
|
+
// Unknown error
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
197
214
|
|
|
198
|
-
|
|
199
|
-
};
|
|
215
|
+
if (!Buffer.isBuffer(response.body)) return true;
|
|
200
216
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
/** @type {string} */
|
|
212
|
-
let encoding = (res.headers['content-encoding']);
|
|
213
|
-
if (encoding == null) {
|
|
214
|
-
// Only continue if unset. Blank is still considered set.
|
|
215
|
-
// This allows forced encoding (eg: use gzip regardless of size; always identity)
|
|
216
|
-
if (inputLength > (this.minimumSize ?? DEFAULT_MINIMUM_SIZE) || transformCount > 1) {
|
|
217
|
-
// If we're getting data in chunks, assume larger than minimum
|
|
218
|
-
encoding = getContentEncoding().toLowerCase?.();
|
|
219
|
-
} else {
|
|
220
|
-
encoding = 'identity';
|
|
217
|
+
/** @return {string} */
|
|
218
|
+
const getContentEncoding = () => {
|
|
219
|
+
if (!parsedEncoding) {
|
|
220
|
+
try {
|
|
221
|
+
parsedEncoding = ContentEncoderMiddleware.chooseEncoding(response.request.headers);
|
|
222
|
+
} catch (error) {
|
|
223
|
+
if (error?.message !== 'NOT_ACCEPTABLE') {
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
221
226
|
}
|
|
222
227
|
}
|
|
228
|
+
if (!parsedEncoding || parsedEncoding === '*') {
|
|
229
|
+
parsedEncoding = this.preferredEncoding || 'identity';
|
|
230
|
+
}
|
|
231
|
+
response.headers['content-encoding'] = parsedEncoding;
|
|
232
|
+
return parsedEncoding;
|
|
233
|
+
};
|
|
223
234
|
|
|
224
|
-
|
|
235
|
+
let encoding = /** @type {string} */ (response.headers['content-encoding']);
|
|
236
|
+
// Only continue if unset (missing header). Blank is still considered set.
|
|
237
|
+
// This allows forced encoding (eg: use gzip regardless of size; always identity)
|
|
238
|
+
|
|
239
|
+
// Unset means server preference
|
|
240
|
+
if (encoding == null) {
|
|
241
|
+
encoding = (response.body.length < this.minimumSize) ? 'identity' : getContentEncoding().toLowerCase?.();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const options = { chunkSize: this.chunkSize };
|
|
245
|
+
|
|
246
|
+
if (response.body.length < this.asyncThreshold) {
|
|
225
247
|
switch (encoding) {
|
|
226
248
|
case 'br':
|
|
249
|
+
response.body = brotliCompressSync(response.body, options);
|
|
250
|
+
break;
|
|
227
251
|
case 'gzip':
|
|
252
|
+
response.body = gzipSync(response.body, options);
|
|
253
|
+
break;
|
|
228
254
|
case 'deflate':
|
|
229
|
-
|
|
255
|
+
response.body = deflateSync(response.body, options);
|
|
230
256
|
break;
|
|
231
257
|
default:
|
|
232
|
-
next = destination;
|
|
233
258
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
});
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
237
261
|
|
|
238
|
-
|
|
239
|
-
|
|
262
|
+
let promise;
|
|
263
|
+
switch (encoding) {
|
|
264
|
+
case 'br':
|
|
265
|
+
promise = ContentEncoderMiddleware.BrotliCompressAsync(response.body, options);
|
|
266
|
+
break;
|
|
267
|
+
case 'gzip':
|
|
268
|
+
promise = ContentEncoderMiddleware.GzipAsync(response.body, options);
|
|
269
|
+
break;
|
|
270
|
+
case 'deflate':
|
|
271
|
+
promise = ContentEncoderMiddleware.DeflateAsync(response.body, options);
|
|
272
|
+
break;
|
|
273
|
+
default:
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
return promise.then((result) => {
|
|
277
|
+
response.body = result;
|
|
278
|
+
return true;
|
|
279
|
+
});
|
|
280
|
+
}
|
|
240
281
|
|
|
241
|
-
|
|
282
|
+
/**
|
|
283
|
+
* Implements `Accept-Encoding`
|
|
284
|
+
* https://tools.ietf.org/html/rfc7231#section-5.3.4
|
|
285
|
+
* @type {MiddlewareFunction}
|
|
286
|
+
*/
|
|
287
|
+
execute({ response }) {
|
|
288
|
+
response.finalizers.push(this.finalizeResponse);
|
|
242
289
|
}
|
|
243
290
|
}
|
|
@@ -1,65 +1,67 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
/** @typedef {import('../types').IMiddleware} IMiddleware */
|
|
1
|
+
/** @typedef {import('../lib/HttpResponse.js').default} HttpResponse */
|
|
4
2
|
/** @typedef {import('../types').MiddlewareFunction} MiddlewareFunction */
|
|
5
|
-
/** @typedef {import('../types').
|
|
6
|
-
|
|
3
|
+
/** @typedef {import('../types').ResponseFinalizer} ResponseFinalizer */
|
|
4
|
+
|
|
5
|
+
import { Transform } from 'node:stream';
|
|
7
6
|
|
|
8
7
|
/**
|
|
9
8
|
* @typedef {Object} ContentLengthMiddlewareOptions
|
|
10
|
-
* @prop {
|
|
11
|
-
* Delays writing to stream by
|
|
9
|
+
* @prop {number} [delayCycles=2]
|
|
10
|
+
* Delays writing to stream by setTimeout cycles when piping.
|
|
12
11
|
* If `.end()` is called on the same event loop as write, then the
|
|
13
12
|
* content length can be still calculated despite receiving data in chunks.
|
|
14
|
-
* Compared to no delay, chunks are held in memory for two event loops instead
|
|
15
|
-
* of just one.
|
|
16
13
|
* @prop {boolean} [overrideHeader=false]
|
|
17
14
|
* Always replace `Content-Length` header
|
|
18
15
|
*/
|
|
19
16
|
|
|
20
|
-
/** @implements {IMiddleware} */
|
|
21
17
|
export default class ContentLengthMiddleware {
|
|
22
18
|
/** @param {ContentLengthMiddlewareOptions} [options] */
|
|
23
19
|
constructor(options = {}) {
|
|
24
|
-
this.
|
|
20
|
+
this.delayCycles = options.delayCycles ?? 2;
|
|
25
21
|
this.overrideHeader = options.overrideHeader !== true;
|
|
22
|
+
this.finalizeResponse = this.finalizeResponse.bind(this);
|
|
26
23
|
}
|
|
27
24
|
|
|
28
25
|
/**
|
|
29
|
-
* @param {
|
|
30
|
-
* @return {
|
|
26
|
+
* @param {HttpResponse} response
|
|
27
|
+
* @return {void}
|
|
31
28
|
*/
|
|
32
|
-
|
|
33
|
-
if (
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
|
|
29
|
+
addTransformStream(response) {
|
|
30
|
+
if (response.headersSent) return;
|
|
31
|
+
let { delayCycles } = this;
|
|
32
|
+
const { overrideHeader } = this;
|
|
33
|
+
if (response.headers['content-length'] && !overrideHeader) return;
|
|
37
34
|
let length = 0;
|
|
38
35
|
/** @type {Buffer[]} */
|
|
39
36
|
const pendingChunks = [];
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const newWritable = new Transform({
|
|
37
|
+
response.pipes.push(new Transform({
|
|
38
|
+
objectMode: true,
|
|
43
39
|
transform(chunk, encoding, callback) {
|
|
44
40
|
length += chunk.length;
|
|
45
|
-
if (
|
|
41
|
+
if (!delayCycles) {
|
|
46
42
|
callback(null, chunk);
|
|
47
43
|
return;
|
|
48
44
|
}
|
|
49
45
|
|
|
50
46
|
pendingChunks.push(chunk);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
.forEach((buffer) => this.push(buffer));
|
|
57
|
-
}));
|
|
47
|
+
// eslint-disable-next-line no-underscore-dangle, unicorn/consistent-function-scoping
|
|
48
|
+
let fn = () => this._flush(() => { /** noop */ });
|
|
49
|
+
for (let i = 0; i < delayCycles; i++) {
|
|
50
|
+
const prev = fn;
|
|
51
|
+
fn = () => setTimeout(prev);
|
|
58
52
|
}
|
|
53
|
+
fn();
|
|
59
54
|
callback();
|
|
60
55
|
},
|
|
61
56
|
flush(callback) {
|
|
62
|
-
|
|
57
|
+
for (const buffer of pendingChunks.splice(0, pendingChunks.length)) {
|
|
58
|
+
this.push(buffer);
|
|
59
|
+
}
|
|
60
|
+
delayCycles = 0;
|
|
61
|
+
callback?.();
|
|
62
|
+
},
|
|
63
|
+
final(callback) {
|
|
64
|
+
if (!response.headersSent) {
|
|
63
65
|
/**
|
|
64
66
|
* Any response message which "MUST NOT" include a message-body
|
|
65
67
|
* (such as the 1xx, 204, and 304 responses and any response to a HEAD request)
|
|
@@ -67,23 +69,46 @@ export default class ContentLengthMiddleware {
|
|
|
67
69
|
* regardless of the entity-header fields present in the message.
|
|
68
70
|
* https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.4
|
|
69
71
|
*/
|
|
70
|
-
if ((
|
|
72
|
+
if ((response.status >= 100 && response.status < 200) || response.status === 204 || response.status === 304) {
|
|
71
73
|
if (overrideHeader) {
|
|
72
|
-
delete
|
|
74
|
+
delete response.headers['content-length'];
|
|
73
75
|
}
|
|
74
|
-
} else if (overrideHeader === true ||
|
|
75
|
-
|
|
76
|
+
} else if (overrideHeader === true || response.headers['content-length'] == null) {
|
|
77
|
+
response.headers['content-length'] = length;
|
|
76
78
|
}
|
|
77
79
|
}
|
|
78
|
-
|
|
79
|
-
.forEach((buffer) => this.push(buffer));
|
|
80
|
-
callback();
|
|
80
|
+
callback?.();
|
|
81
81
|
},
|
|
82
|
-
});
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
83
84
|
|
|
84
|
-
|
|
85
|
-
|
|
85
|
+
/** @type {ResponseFinalizer} */
|
|
86
|
+
finalizeResponse(response) {
|
|
87
|
+
if (response.headersSent) return;
|
|
88
|
+
if (response.isStreaming) {
|
|
89
|
+
this.addTransformStream(response);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (!Buffer.isBuffer(response.body)) return;
|
|
93
|
+
if (!response.body.byteLength) return;
|
|
94
|
+
/**
|
|
95
|
+
* Any response message which "MUST NOT" include a message-body
|
|
96
|
+
* (such as the 1xx, 204, and 304 responses and any response to a HEAD request)
|
|
97
|
+
* is always terminated by the first empty line after the header fields,
|
|
98
|
+
* regardless of the entity-header fields present in the message.
|
|
99
|
+
* https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.4
|
|
100
|
+
*/
|
|
101
|
+
if (response.status === 204 || response.status === 304 || (response.status >= 100 && response.status < 200)) {
|
|
102
|
+
if (this.overrideHeader) {
|
|
103
|
+
delete response.headers['content-length'];
|
|
104
|
+
}
|
|
105
|
+
} else if (this.overrideHeader === true || response.headers['content-length'] == null) {
|
|
106
|
+
response.headers['content-length'] = response.body.byteLength;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
86
109
|
|
|
87
|
-
|
|
110
|
+
/** @type {MiddlewareFunction} */
|
|
111
|
+
execute({ response }) {
|
|
112
|
+
response.finalizers.push(this.finalizeResponse);
|
|
88
113
|
}
|
|
89
114
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Transform } from 'stream';
|
|
1
|
+
import { Transform } from 'node:stream';
|
|
2
2
|
|
|
3
3
|
/** @typedef {import('../types').IMiddleware} IMiddleware */
|
|
4
4
|
/** @typedef {import('../types').MiddlewareFunction} MiddlewareFunction */
|
|
@@ -42,13 +42,13 @@ export default class ContentWriterMiddleware {
|
|
|
42
42
|
case 'ucs2':
|
|
43
43
|
case 'utf16le':
|
|
44
44
|
return 'utf16le';
|
|
45
|
-
default:
|
|
46
|
-
case 'utf-8':
|
|
47
|
-
case 'utf8':
|
|
48
|
-
return 'utf-8';
|
|
49
45
|
case 'base64':
|
|
50
46
|
case 'hex':
|
|
51
47
|
return /** @type {BufferEncoding} */ (charset);
|
|
48
|
+
case 'utf-8':
|
|
49
|
+
case 'utf8':
|
|
50
|
+
default:
|
|
51
|
+
return 'utf-8';
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|