@whatwg-node/server 0.10.0-alpha-20240717144936-662ea5088eb83ec5b55025307193fac81a8ea190 → 0.10.0-alpha-20240726141316-c6ce93b3598457ebe73b3b725986723af8f5e609
Sign up to get free protection for your applications and to get access to all the features.
- package/cjs/createServerAdapter.js +9 -1
- package/cjs/index.js +1 -0
- package/cjs/plugins/useContentEncoding.js +107 -0
- package/cjs/utils.js +75 -9
- package/cjs/uwebsockets.js +37 -10
- package/esm/createServerAdapter.js +9 -1
- package/esm/index.js +1 -0
- package/esm/plugins/useContentEncoding.js +104 -0
- package/esm/utils.js +72 -8
- package/esm/uwebsockets.js +37 -10
- package/package.json +2 -2
- package/typings/index.d.cts +1 -0
- package/typings/index.d.ts +1 -0
- package/typings/plugins/types.d.cts +5 -2
- package/typings/plugins/types.d.ts +5 -2
- package/typings/plugins/useContentEncoding.d.cts +2 -0
- package/typings/plugins/useContentEncoding.d.ts +2 -0
- package/typings/plugins/useErrorHandling.d.cts +2 -2
- package/typings/plugins/useErrorHandling.d.ts +2 -2
- package/typings/utils.d.cts +4 -1
- package/typings/utils.d.ts +4 -1
@@ -54,6 +54,9 @@ function createServerAdapter(serverAdapterBaseObject, options) {
|
|
54
54
|
});
|
55
55
|
const onRequestHooksIteration$ = (0, utils_js_1.iterateAsyncVoid)(onRequestHooks, (onRequestHook, stopEarly) => onRequestHook({
|
56
56
|
request,
|
57
|
+
setRequest(newRequest) {
|
58
|
+
request = newRequest;
|
59
|
+
},
|
57
60
|
serverContext,
|
58
61
|
fetchAPI,
|
59
62
|
url,
|
@@ -69,13 +72,17 @@ function createServerAdapter(serverAdapterBaseObject, options) {
|
|
69
72
|
},
|
70
73
|
}));
|
71
74
|
function handleResponse(response) {
|
72
|
-
if (
|
75
|
+
if (onResponseHooks.length === 0) {
|
73
76
|
return response;
|
74
77
|
}
|
75
78
|
const onResponseHookPayload = {
|
76
79
|
request,
|
77
80
|
response,
|
78
81
|
serverContext,
|
82
|
+
setResponse(newResponse) {
|
83
|
+
response = newResponse;
|
84
|
+
},
|
85
|
+
fetchAPI,
|
79
86
|
};
|
80
87
|
const onResponseHooksIteration$ = (0, utils_js_1.iterateAsyncVoid)(onResponseHooks, onResponseHook => onResponseHook(onResponseHookPayload));
|
81
88
|
if ((0, utils_js_1.isPromise)(onResponseHooksIteration$)) {
|
@@ -99,6 +106,7 @@ function createServerAdapter(serverAdapterBaseObject, options) {
|
|
99
106
|
return handleEarlyResponse();
|
100
107
|
}
|
101
108
|
: givenHandleRequest;
|
109
|
+
// TODO: Remove this on the next major version
|
102
110
|
function handleNodeRequestAndResponse(nodeRequest, nodeResponseOrContainer, ...ctx) {
|
103
111
|
const nodeResponse = nodeResponseOrContainer.raw || nodeResponseOrContainer;
|
104
112
|
const serverContext = ctx.length > 1 ? (0, utils_js_1.completeAssign)(...ctx) : ctx[0] || {};
|
package/cjs/index.js
CHANGED
@@ -8,6 +8,7 @@ tslib_1.__exportStar(require("./utils.js"), exports);
|
|
8
8
|
tslib_1.__exportStar(require("./plugins/types.js"), exports);
|
9
9
|
tslib_1.__exportStar(require("./plugins/useCors.js"), exports);
|
10
10
|
tslib_1.__exportStar(require("./plugins/useErrorHandling.js"), exports);
|
11
|
+
tslib_1.__exportStar(require("./plugins/useContentEncoding.js"), exports);
|
11
12
|
tslib_1.__exportStar(require("./uwebsockets.js"), exports);
|
12
13
|
var fetch_1 = require("@whatwg-node/fetch");
|
13
14
|
Object.defineProperty(exports, "Response", { enumerable: true, get: function () { return fetch_1.Response; } });
|
@@ -0,0 +1,107 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.useContentEncoding = useContentEncoding;
|
4
|
+
const utils_js_1 = require("../utils.js");
|
5
|
+
function useContentEncoding() {
|
6
|
+
const encodingMap = new WeakMap();
|
7
|
+
return {
|
8
|
+
onRequest({ request, setRequest, fetchAPI, endResponse }) {
|
9
|
+
if (request.body) {
|
10
|
+
const contentEncodingHeader = request.headers.get('content-encoding');
|
11
|
+
if (contentEncodingHeader && contentEncodingHeader !== 'none') {
|
12
|
+
const contentEncodings = contentEncodingHeader?.split(',');
|
13
|
+
if (!contentEncodings.every(encoding => (0, utils_js_1.getSupportedEncodings)(fetchAPI).includes(encoding))) {
|
14
|
+
endResponse(new fetchAPI.Response(`Unsupported 'Content-Encoding': ${contentEncodingHeader}`, {
|
15
|
+
status: 415,
|
16
|
+
statusText: 'Unsupported Media Type',
|
17
|
+
}));
|
18
|
+
return;
|
19
|
+
}
|
20
|
+
let newBody = request.body;
|
21
|
+
for (const contentEncoding of contentEncodings) {
|
22
|
+
newBody = newBody.pipeThrough(new fetchAPI.DecompressionStream(contentEncoding));
|
23
|
+
}
|
24
|
+
const newRequest = new fetchAPI.Request(request.url, {
|
25
|
+
body: newBody,
|
26
|
+
cache: request.cache,
|
27
|
+
credentials: request.credentials,
|
28
|
+
headers: request.headers,
|
29
|
+
integrity: request.integrity,
|
30
|
+
keepalive: request.keepalive,
|
31
|
+
method: request.method,
|
32
|
+
mode: request.mode,
|
33
|
+
redirect: request.redirect,
|
34
|
+
referrer: request.referrer,
|
35
|
+
referrerPolicy: request.referrerPolicy,
|
36
|
+
signal: request.signal,
|
37
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
38
|
+
// @ts-ignore - not in the TS types yet
|
39
|
+
duplex: 'half',
|
40
|
+
});
|
41
|
+
setRequest(newRequest);
|
42
|
+
}
|
43
|
+
}
|
44
|
+
const acceptEncoding = request.headers.get('accept-encoding');
|
45
|
+
if (acceptEncoding) {
|
46
|
+
encodingMap.set(request, acceptEncoding.split(','));
|
47
|
+
}
|
48
|
+
},
|
49
|
+
onResponse({ request, response, setResponse, fetchAPI }) {
|
50
|
+
// Hack for avoiding to create whatwg-node to create a readable stream until it's needed
|
51
|
+
if (response['bodyInit'] || response.body) {
|
52
|
+
const encodings = encodingMap.get(request);
|
53
|
+
if (encodings) {
|
54
|
+
const supportedEncoding = encodings.find(encoding => (0, utils_js_1.getSupportedEncodings)(fetchAPI).includes(encoding));
|
55
|
+
if (supportedEncoding) {
|
56
|
+
const compressionStream = new fetchAPI.CompressionStream(supportedEncoding);
|
57
|
+
// To calculate final content-length
|
58
|
+
const contentLength = response.headers.get('content-length');
|
59
|
+
if (contentLength) {
|
60
|
+
const bufOfRes = response._buffer;
|
61
|
+
if (bufOfRes) {
|
62
|
+
const writer = compressionStream.writable.getWriter();
|
63
|
+
writer.write(bufOfRes);
|
64
|
+
writer.close();
|
65
|
+
const reader = compressionStream.readable.getReader();
|
66
|
+
return Promise.resolve().then(async () => {
|
67
|
+
const chunks = [];
|
68
|
+
while (true) {
|
69
|
+
const { done, value } = await reader.read();
|
70
|
+
if (done) {
|
71
|
+
reader.releaseLock();
|
72
|
+
break;
|
73
|
+
}
|
74
|
+
else if (value) {
|
75
|
+
chunks.push(...value);
|
76
|
+
}
|
77
|
+
}
|
78
|
+
const uint8Array = new Uint8Array(chunks);
|
79
|
+
const newHeaders = new fetchAPI.Headers(response.headers);
|
80
|
+
newHeaders.set('content-encoding', supportedEncoding);
|
81
|
+
newHeaders.set('content-length', uint8Array.byteLength.toString());
|
82
|
+
const compressedResponse = new fetchAPI.Response(uint8Array, {
|
83
|
+
...response,
|
84
|
+
headers: newHeaders,
|
85
|
+
});
|
86
|
+
utils_js_1.decompressedResponseMap.set(compressedResponse, response);
|
87
|
+
setResponse(compressedResponse);
|
88
|
+
});
|
89
|
+
}
|
90
|
+
}
|
91
|
+
const newHeaders = new fetchAPI.Headers(response.headers);
|
92
|
+
newHeaders.set('content-encoding', supportedEncoding);
|
93
|
+
newHeaders.delete('content-length');
|
94
|
+
const compressedBody = response.body.pipeThrough(compressionStream);
|
95
|
+
const compressedResponse = new fetchAPI.Response(compressedBody, {
|
96
|
+
status: response.status,
|
97
|
+
statusText: response.statusText,
|
98
|
+
headers: newHeaders,
|
99
|
+
});
|
100
|
+
utils_js_1.decompressedResponseMap.set(compressedResponse, response);
|
101
|
+
setResponse(compressedResponse);
|
102
|
+
}
|
103
|
+
}
|
104
|
+
}
|
105
|
+
},
|
106
|
+
};
|
107
|
+
}
|
package/cjs/utils.js
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
"use strict";
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
-
exports.ServerAdapterRequestAbortSignal = void 0;
|
3
|
+
exports.decompressedResponseMap = exports.ServerAdapterRequestAbortSignal = void 0;
|
4
4
|
exports.isAsyncIterable = isAsyncIterable;
|
5
5
|
exports.normalizeNodeRequest = normalizeNodeRequest;
|
6
6
|
exports.isReadable = isReadable;
|
@@ -17,6 +17,8 @@ exports.handleErrorFromRequestHandler = handleErrorFromRequestHandler;
|
|
17
17
|
exports.isolateObject = isolateObject;
|
18
18
|
exports.createDeferredPromise = createDeferredPromise;
|
19
19
|
exports.handleAbortSignalAndPromiseResponse = handleAbortSignalAndPromiseResponse;
|
20
|
+
exports.getSupportedEncodings = getSupportedEncodings;
|
21
|
+
exports.handleResponseDecompression = handleResponseDecompression;
|
20
22
|
const fetch_1 = require("@whatwg-node/fetch");
|
21
23
|
function isAsyncIterable(body) {
|
22
24
|
return (body != null && typeof body === 'object' && typeof body[Symbol.asyncIterator] === 'function');
|
@@ -112,7 +114,16 @@ function normalizeNodeRequest(nodeRequest, nodeResponse, RequestCtor) {
|
|
112
114
|
fullUrl = url.toString();
|
113
115
|
}
|
114
116
|
let signal;
|
115
|
-
|
117
|
+
let normalizedHeaders = nodeRequest.headers;
|
118
|
+
if (nodeRequest.headers?.[':method']) {
|
119
|
+
normalizedHeaders = {};
|
120
|
+
for (const key in nodeRequest.headers) {
|
121
|
+
if (!key.startsWith(':')) {
|
122
|
+
normalizedHeaders[key] = nodeRequest.headers[key];
|
123
|
+
}
|
124
|
+
}
|
125
|
+
}
|
126
|
+
if (nodeResponse?.once) {
|
116
127
|
let sendAbortSignal;
|
117
128
|
// If ponyfilled
|
118
129
|
if (RequestCtor !== globalThis.Request) {
|
@@ -139,7 +150,7 @@ function normalizeNodeRequest(nodeRequest, nodeResponse, RequestCtor) {
|
|
139
150
|
if (nodeRequest.method === 'GET' || nodeRequest.method === 'HEAD') {
|
140
151
|
return new RequestCtor(fullUrl, {
|
141
152
|
method: nodeRequest.method,
|
142
|
-
headers:
|
153
|
+
headers: normalizedHeaders,
|
143
154
|
signal,
|
144
155
|
});
|
145
156
|
}
|
@@ -154,14 +165,14 @@ function normalizeNodeRequest(nodeRequest, nodeResponse, RequestCtor) {
|
|
154
165
|
if (isRequestBody(maybeParsedBody)) {
|
155
166
|
return new RequestCtor(fullUrl, {
|
156
167
|
method: nodeRequest.method,
|
157
|
-
headers:
|
168
|
+
headers: normalizedHeaders,
|
158
169
|
body: maybeParsedBody,
|
159
170
|
signal,
|
160
171
|
});
|
161
172
|
}
|
162
173
|
const request = new RequestCtor(fullUrl, {
|
163
174
|
method: nodeRequest.method,
|
164
|
-
headers:
|
175
|
+
headers: normalizedHeaders,
|
165
176
|
signal,
|
166
177
|
});
|
167
178
|
if (!request.headers.get('content-type')?.includes('json')) {
|
@@ -189,7 +200,7 @@ It will affect your performance. Please check our Bun integration recipe, and av
|
|
189
200
|
}
|
190
201
|
return new RequestCtor(fullUrl, {
|
191
202
|
method: nodeRequest.method,
|
192
|
-
headers:
|
203
|
+
headers: normalizedHeaders,
|
193
204
|
duplex: 'half',
|
194
205
|
body: new ReadableStream({
|
195
206
|
start(controller) {
|
@@ -213,7 +224,7 @@ It will affect your performance. Please check our Bun integration recipe, and av
|
|
213
224
|
// perf: instead of spreading the object, we can just pass it as is and it performs better
|
214
225
|
return new RequestCtor(fullUrl, {
|
215
226
|
method: nodeRequest.method,
|
216
|
-
headers:
|
227
|
+
headers: normalizedHeaders,
|
217
228
|
body: rawRequest,
|
218
229
|
duplex: 'half',
|
219
230
|
signal,
|
@@ -341,13 +352,16 @@ function completeAssign(...args) {
|
|
341
352
|
// modified Object.keys to Object.getOwnPropertyNames
|
342
353
|
// because Object.keys only returns enumerable properties
|
343
354
|
const descriptors = Object.getOwnPropertyNames(source).reduce((descriptors, key) => {
|
344
|
-
|
355
|
+
const descriptor = Object.getOwnPropertyDescriptor(source, key);
|
356
|
+
if (descriptor) {
|
357
|
+
descriptors[key] = Object.getOwnPropertyDescriptor(source, key);
|
358
|
+
}
|
345
359
|
return descriptors;
|
346
360
|
}, {});
|
347
361
|
// By default, Object.assign copies enumerable Symbols, too
|
348
362
|
Object.getOwnPropertySymbols(source).forEach(sym => {
|
349
363
|
const descriptor = Object.getOwnPropertyDescriptor(source, sym);
|
350
|
-
if (descriptor
|
364
|
+
if (descriptor?.enumerable) {
|
351
365
|
descriptors[sym] = descriptor;
|
352
366
|
}
|
353
367
|
});
|
@@ -496,3 +510,55 @@ function handleAbortSignalAndPromiseResponse(response$, abortSignal) {
|
|
496
510
|
}
|
497
511
|
return response$;
|
498
512
|
}
|
513
|
+
exports.decompressedResponseMap = new WeakMap();
|
514
|
+
const supportedEncodingsByFetchAPI = new WeakMap();
|
515
|
+
function getSupportedEncodings(fetchAPI) {
|
516
|
+
let supportedEncodings = supportedEncodingsByFetchAPI.get(fetchAPI);
|
517
|
+
if (!supportedEncodings) {
|
518
|
+
const possibleEncodings = ['deflate', 'gzip', 'deflate-raw', 'br'];
|
519
|
+
supportedEncodings = possibleEncodings.filter(encoding => {
|
520
|
+
// deflate-raw is not supported in Node.js >v20
|
521
|
+
if (globalThis.process?.version?.startsWith('v2') &&
|
522
|
+
fetchAPI.DecompressionStream === globalThis.DecompressionStream &&
|
523
|
+
encoding === 'deflate-raw') {
|
524
|
+
return false;
|
525
|
+
}
|
526
|
+
try {
|
527
|
+
// eslint-disable-next-line no-new
|
528
|
+
new fetchAPI.DecompressionStream(encoding);
|
529
|
+
return true;
|
530
|
+
}
|
531
|
+
catch {
|
532
|
+
return false;
|
533
|
+
}
|
534
|
+
});
|
535
|
+
supportedEncodingsByFetchAPI.set(fetchAPI, supportedEncodings);
|
536
|
+
}
|
537
|
+
return supportedEncodings;
|
538
|
+
}
|
539
|
+
function handleResponseDecompression(response, fetchAPI) {
|
540
|
+
const contentEncodingHeader = response?.headers.get('content-encoding');
|
541
|
+
if (!contentEncodingHeader || contentEncodingHeader === 'none') {
|
542
|
+
return response;
|
543
|
+
}
|
544
|
+
if (!response?.body) {
|
545
|
+
return response;
|
546
|
+
}
|
547
|
+
let decompressedResponse = exports.decompressedResponseMap.get(response);
|
548
|
+
if (!decompressedResponse || decompressedResponse.bodyUsed) {
|
549
|
+
let decompressedBody = response.body;
|
550
|
+
const contentEncodings = contentEncodingHeader.split(',');
|
551
|
+
if (!contentEncodings.every(encoding => getSupportedEncodings(fetchAPI).includes(encoding))) {
|
552
|
+
return new fetchAPI.Response(`Unsupported 'Content-Encoding': ${contentEncodingHeader}`, {
|
553
|
+
status: 415,
|
554
|
+
statusText: 'Unsupported Media Type',
|
555
|
+
});
|
556
|
+
}
|
557
|
+
for (const contentEncoding of contentEncodings) {
|
558
|
+
decompressedBody = decompressedBody.pipeThrough(new fetchAPI.DecompressionStream(contentEncoding));
|
559
|
+
}
|
560
|
+
decompressedResponse = new fetchAPI.Response(decompressedBody, response);
|
561
|
+
exports.decompressedResponseMap.set(response, decompressedResponse);
|
562
|
+
}
|
563
|
+
return decompressedResponse;
|
564
|
+
}
|
package/cjs/uwebsockets.js
CHANGED
@@ -10,18 +10,42 @@ function getRequestFromUWSRequest({ req, res, fetchAPI, signal }) {
|
|
10
10
|
let body;
|
11
11
|
const method = req.getMethod();
|
12
12
|
if (method !== 'get' && method !== 'head') {
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
13
|
+
let controller;
|
14
|
+
body = new fetchAPI.ReadableStream({
|
15
|
+
start(c) {
|
16
|
+
controller = c;
|
17
|
+
},
|
17
18
|
});
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
if (isLast) {
|
19
|
+
const readable = body.readable;
|
20
|
+
if (readable) {
|
21
|
+
signal.addEventListener('abort', () => {
|
22
22
|
readable.push(null);
|
23
|
-
}
|
24
|
-
|
23
|
+
});
|
24
|
+
res.onData(function (ab, isLast) {
|
25
|
+
const chunk = Buffer.from(ab, 0, ab.byteLength);
|
26
|
+
readable.push(Buffer.from(chunk));
|
27
|
+
if (isLast) {
|
28
|
+
readable.push(null);
|
29
|
+
}
|
30
|
+
});
|
31
|
+
}
|
32
|
+
else {
|
33
|
+
let closed = false;
|
34
|
+
signal.addEventListener('abort', () => {
|
35
|
+
if (!closed) {
|
36
|
+
closed = true;
|
37
|
+
controller.close();
|
38
|
+
}
|
39
|
+
});
|
40
|
+
res.onData(function (ab, isLast) {
|
41
|
+
const chunk = Buffer.from(ab, 0, ab.byteLength);
|
42
|
+
controller.enqueue(Buffer.from(chunk));
|
43
|
+
if (isLast) {
|
44
|
+
closed = true;
|
45
|
+
controller.close();
|
46
|
+
}
|
47
|
+
});
|
48
|
+
}
|
25
49
|
}
|
26
50
|
const headers = new fetchAPI.Headers();
|
27
51
|
req.forEach((key, value) => {
|
@@ -37,6 +61,9 @@ function getRequestFromUWSRequest({ req, res, fetchAPI, signal }) {
|
|
37
61
|
headers,
|
38
62
|
body: body,
|
39
63
|
signal,
|
64
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
65
|
+
// @ts-ignore - not in the TS types yet
|
66
|
+
duplex: 'half',
|
40
67
|
});
|
41
68
|
}
|
42
69
|
async function forwardResponseBodyToUWSResponse(uwsResponse, fetchResponse, signal) {
|
@@ -50,6 +50,9 @@ function createServerAdapter(serverAdapterBaseObject, options) {
|
|
50
50
|
});
|
51
51
|
const onRequestHooksIteration$ = iterateAsyncVoid(onRequestHooks, (onRequestHook, stopEarly) => onRequestHook({
|
52
52
|
request,
|
53
|
+
setRequest(newRequest) {
|
54
|
+
request = newRequest;
|
55
|
+
},
|
53
56
|
serverContext,
|
54
57
|
fetchAPI,
|
55
58
|
url,
|
@@ -65,13 +68,17 @@ function createServerAdapter(serverAdapterBaseObject, options) {
|
|
65
68
|
},
|
66
69
|
}));
|
67
70
|
function handleResponse(response) {
|
68
|
-
if (
|
71
|
+
if (onResponseHooks.length === 0) {
|
69
72
|
return response;
|
70
73
|
}
|
71
74
|
const onResponseHookPayload = {
|
72
75
|
request,
|
73
76
|
response,
|
74
77
|
serverContext,
|
78
|
+
setResponse(newResponse) {
|
79
|
+
response = newResponse;
|
80
|
+
},
|
81
|
+
fetchAPI,
|
75
82
|
};
|
76
83
|
const onResponseHooksIteration$ = iterateAsyncVoid(onResponseHooks, onResponseHook => onResponseHook(onResponseHookPayload));
|
77
84
|
if (isPromise(onResponseHooksIteration$)) {
|
@@ -95,6 +102,7 @@ function createServerAdapter(serverAdapterBaseObject, options) {
|
|
95
102
|
return handleEarlyResponse();
|
96
103
|
}
|
97
104
|
: givenHandleRequest;
|
105
|
+
// TODO: Remove this on the next major version
|
98
106
|
function handleNodeRequestAndResponse(nodeRequest, nodeResponseOrContainer, ...ctx) {
|
99
107
|
const nodeResponse = nodeResponseOrContainer.raw || nodeResponseOrContainer;
|
100
108
|
const serverContext = ctx.length > 1 ? completeAssign(...ctx) : ctx[0] || {};
|
package/esm/index.js
CHANGED
@@ -4,5 +4,6 @@ export * from './utils.js';
|
|
4
4
|
export * from './plugins/types.js';
|
5
5
|
export * from './plugins/useCors.js';
|
6
6
|
export * from './plugins/useErrorHandling.js';
|
7
|
+
export * from './plugins/useContentEncoding.js';
|
7
8
|
export * from './uwebsockets.js';
|
8
9
|
export { Response } from '@whatwg-node/fetch';
|
@@ -0,0 +1,104 @@
|
|
1
|
+
import { decompressedResponseMap, getSupportedEncodings } from '../utils.js';
|
2
|
+
export function useContentEncoding() {
|
3
|
+
const encodingMap = new WeakMap();
|
4
|
+
return {
|
5
|
+
onRequest({ request, setRequest, fetchAPI, endResponse }) {
|
6
|
+
if (request.body) {
|
7
|
+
const contentEncodingHeader = request.headers.get('content-encoding');
|
8
|
+
if (contentEncodingHeader && contentEncodingHeader !== 'none') {
|
9
|
+
const contentEncodings = contentEncodingHeader?.split(',');
|
10
|
+
if (!contentEncodings.every(encoding => getSupportedEncodings(fetchAPI).includes(encoding))) {
|
11
|
+
endResponse(new fetchAPI.Response(`Unsupported 'Content-Encoding': ${contentEncodingHeader}`, {
|
12
|
+
status: 415,
|
13
|
+
statusText: 'Unsupported Media Type',
|
14
|
+
}));
|
15
|
+
return;
|
16
|
+
}
|
17
|
+
let newBody = request.body;
|
18
|
+
for (const contentEncoding of contentEncodings) {
|
19
|
+
newBody = newBody.pipeThrough(new fetchAPI.DecompressionStream(contentEncoding));
|
20
|
+
}
|
21
|
+
const newRequest = new fetchAPI.Request(request.url, {
|
22
|
+
body: newBody,
|
23
|
+
cache: request.cache,
|
24
|
+
credentials: request.credentials,
|
25
|
+
headers: request.headers,
|
26
|
+
integrity: request.integrity,
|
27
|
+
keepalive: request.keepalive,
|
28
|
+
method: request.method,
|
29
|
+
mode: request.mode,
|
30
|
+
redirect: request.redirect,
|
31
|
+
referrer: request.referrer,
|
32
|
+
referrerPolicy: request.referrerPolicy,
|
33
|
+
signal: request.signal,
|
34
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
35
|
+
// @ts-ignore - not in the TS types yet
|
36
|
+
duplex: 'half',
|
37
|
+
});
|
38
|
+
setRequest(newRequest);
|
39
|
+
}
|
40
|
+
}
|
41
|
+
const acceptEncoding = request.headers.get('accept-encoding');
|
42
|
+
if (acceptEncoding) {
|
43
|
+
encodingMap.set(request, acceptEncoding.split(','));
|
44
|
+
}
|
45
|
+
},
|
46
|
+
onResponse({ request, response, setResponse, fetchAPI }) {
|
47
|
+
// Hack for avoiding to create whatwg-node to create a readable stream until it's needed
|
48
|
+
if (response['bodyInit'] || response.body) {
|
49
|
+
const encodings = encodingMap.get(request);
|
50
|
+
if (encodings) {
|
51
|
+
const supportedEncoding = encodings.find(encoding => getSupportedEncodings(fetchAPI).includes(encoding));
|
52
|
+
if (supportedEncoding) {
|
53
|
+
const compressionStream = new fetchAPI.CompressionStream(supportedEncoding);
|
54
|
+
// To calculate final content-length
|
55
|
+
const contentLength = response.headers.get('content-length');
|
56
|
+
if (contentLength) {
|
57
|
+
const bufOfRes = response._buffer;
|
58
|
+
if (bufOfRes) {
|
59
|
+
const writer = compressionStream.writable.getWriter();
|
60
|
+
writer.write(bufOfRes);
|
61
|
+
writer.close();
|
62
|
+
const reader = compressionStream.readable.getReader();
|
63
|
+
return Promise.resolve().then(async () => {
|
64
|
+
const chunks = [];
|
65
|
+
while (true) {
|
66
|
+
const { done, value } = await reader.read();
|
67
|
+
if (done) {
|
68
|
+
reader.releaseLock();
|
69
|
+
break;
|
70
|
+
}
|
71
|
+
else if (value) {
|
72
|
+
chunks.push(...value);
|
73
|
+
}
|
74
|
+
}
|
75
|
+
const uint8Array = new Uint8Array(chunks);
|
76
|
+
const newHeaders = new fetchAPI.Headers(response.headers);
|
77
|
+
newHeaders.set('content-encoding', supportedEncoding);
|
78
|
+
newHeaders.set('content-length', uint8Array.byteLength.toString());
|
79
|
+
const compressedResponse = new fetchAPI.Response(uint8Array, {
|
80
|
+
...response,
|
81
|
+
headers: newHeaders,
|
82
|
+
});
|
83
|
+
decompressedResponseMap.set(compressedResponse, response);
|
84
|
+
setResponse(compressedResponse);
|
85
|
+
});
|
86
|
+
}
|
87
|
+
}
|
88
|
+
const newHeaders = new fetchAPI.Headers(response.headers);
|
89
|
+
newHeaders.set('content-encoding', supportedEncoding);
|
90
|
+
newHeaders.delete('content-length');
|
91
|
+
const compressedBody = response.body.pipeThrough(compressionStream);
|
92
|
+
const compressedResponse = new fetchAPI.Response(compressedBody, {
|
93
|
+
status: response.status,
|
94
|
+
statusText: response.statusText,
|
95
|
+
headers: newHeaders,
|
96
|
+
});
|
97
|
+
decompressedResponseMap.set(compressedResponse, response);
|
98
|
+
setResponse(compressedResponse);
|
99
|
+
}
|
100
|
+
}
|
101
|
+
}
|
102
|
+
},
|
103
|
+
};
|
104
|
+
}
|
package/esm/utils.js
CHANGED
@@ -92,7 +92,16 @@ export function normalizeNodeRequest(nodeRequest, nodeResponse, RequestCtor) {
|
|
92
92
|
fullUrl = url.toString();
|
93
93
|
}
|
94
94
|
let signal;
|
95
|
-
|
95
|
+
let normalizedHeaders = nodeRequest.headers;
|
96
|
+
if (nodeRequest.headers?.[':method']) {
|
97
|
+
normalizedHeaders = {};
|
98
|
+
for (const key in nodeRequest.headers) {
|
99
|
+
if (!key.startsWith(':')) {
|
100
|
+
normalizedHeaders[key] = nodeRequest.headers[key];
|
101
|
+
}
|
102
|
+
}
|
103
|
+
}
|
104
|
+
if (nodeResponse?.once) {
|
96
105
|
let sendAbortSignal;
|
97
106
|
// If ponyfilled
|
98
107
|
if (RequestCtor !== globalThis.Request) {
|
@@ -119,7 +128,7 @@ export function normalizeNodeRequest(nodeRequest, nodeResponse, RequestCtor) {
|
|
119
128
|
if (nodeRequest.method === 'GET' || nodeRequest.method === 'HEAD') {
|
120
129
|
return new RequestCtor(fullUrl, {
|
121
130
|
method: nodeRequest.method,
|
122
|
-
headers:
|
131
|
+
headers: normalizedHeaders,
|
123
132
|
signal,
|
124
133
|
});
|
125
134
|
}
|
@@ -134,14 +143,14 @@ export function normalizeNodeRequest(nodeRequest, nodeResponse, RequestCtor) {
|
|
134
143
|
if (isRequestBody(maybeParsedBody)) {
|
135
144
|
return new RequestCtor(fullUrl, {
|
136
145
|
method: nodeRequest.method,
|
137
|
-
headers:
|
146
|
+
headers: normalizedHeaders,
|
138
147
|
body: maybeParsedBody,
|
139
148
|
signal,
|
140
149
|
});
|
141
150
|
}
|
142
151
|
const request = new RequestCtor(fullUrl, {
|
143
152
|
method: nodeRequest.method,
|
144
|
-
headers:
|
153
|
+
headers: normalizedHeaders,
|
145
154
|
signal,
|
146
155
|
});
|
147
156
|
if (!request.headers.get('content-type')?.includes('json')) {
|
@@ -169,7 +178,7 @@ It will affect your performance. Please check our Bun integration recipe, and av
|
|
169
178
|
}
|
170
179
|
return new RequestCtor(fullUrl, {
|
171
180
|
method: nodeRequest.method,
|
172
|
-
headers:
|
181
|
+
headers: normalizedHeaders,
|
173
182
|
duplex: 'half',
|
174
183
|
body: new ReadableStream({
|
175
184
|
start(controller) {
|
@@ -193,7 +202,7 @@ It will affect your performance. Please check our Bun integration recipe, and av
|
|
193
202
|
// perf: instead of spreading the object, we can just pass it as is and it performs better
|
194
203
|
return new RequestCtor(fullUrl, {
|
195
204
|
method: nodeRequest.method,
|
196
|
-
headers:
|
205
|
+
headers: normalizedHeaders,
|
197
206
|
body: rawRequest,
|
198
207
|
duplex: 'half',
|
199
208
|
signal,
|
@@ -321,13 +330,16 @@ export function completeAssign(...args) {
|
|
321
330
|
// modified Object.keys to Object.getOwnPropertyNames
|
322
331
|
// because Object.keys only returns enumerable properties
|
323
332
|
const descriptors = Object.getOwnPropertyNames(source).reduce((descriptors, key) => {
|
324
|
-
|
333
|
+
const descriptor = Object.getOwnPropertyDescriptor(source, key);
|
334
|
+
if (descriptor) {
|
335
|
+
descriptors[key] = Object.getOwnPropertyDescriptor(source, key);
|
336
|
+
}
|
325
337
|
return descriptors;
|
326
338
|
}, {});
|
327
339
|
// By default, Object.assign copies enumerable Symbols, too
|
328
340
|
Object.getOwnPropertySymbols(source).forEach(sym => {
|
329
341
|
const descriptor = Object.getOwnPropertyDescriptor(source, sym);
|
330
|
-
if (descriptor
|
342
|
+
if (descriptor?.enumerable) {
|
331
343
|
descriptors[sym] = descriptor;
|
332
344
|
}
|
333
345
|
});
|
@@ -476,3 +488,55 @@ export function handleAbortSignalAndPromiseResponse(response$, abortSignal) {
|
|
476
488
|
}
|
477
489
|
return response$;
|
478
490
|
}
|
491
|
+
export const decompressedResponseMap = new WeakMap();
|
492
|
+
const supportedEncodingsByFetchAPI = new WeakMap();
|
493
|
+
export function getSupportedEncodings(fetchAPI) {
|
494
|
+
let supportedEncodings = supportedEncodingsByFetchAPI.get(fetchAPI);
|
495
|
+
if (!supportedEncodings) {
|
496
|
+
const possibleEncodings = ['deflate', 'gzip', 'deflate-raw', 'br'];
|
497
|
+
supportedEncodings = possibleEncodings.filter(encoding => {
|
498
|
+
// deflate-raw is not supported in Node.js >v20
|
499
|
+
if (globalThis.process?.version?.startsWith('v2') &&
|
500
|
+
fetchAPI.DecompressionStream === globalThis.DecompressionStream &&
|
501
|
+
encoding === 'deflate-raw') {
|
502
|
+
return false;
|
503
|
+
}
|
504
|
+
try {
|
505
|
+
// eslint-disable-next-line no-new
|
506
|
+
new fetchAPI.DecompressionStream(encoding);
|
507
|
+
return true;
|
508
|
+
}
|
509
|
+
catch {
|
510
|
+
return false;
|
511
|
+
}
|
512
|
+
});
|
513
|
+
supportedEncodingsByFetchAPI.set(fetchAPI, supportedEncodings);
|
514
|
+
}
|
515
|
+
return supportedEncodings;
|
516
|
+
}
|
517
|
+
export function handleResponseDecompression(response, fetchAPI) {
|
518
|
+
const contentEncodingHeader = response?.headers.get('content-encoding');
|
519
|
+
if (!contentEncodingHeader || contentEncodingHeader === 'none') {
|
520
|
+
return response;
|
521
|
+
}
|
522
|
+
if (!response?.body) {
|
523
|
+
return response;
|
524
|
+
}
|
525
|
+
let decompressedResponse = decompressedResponseMap.get(response);
|
526
|
+
if (!decompressedResponse || decompressedResponse.bodyUsed) {
|
527
|
+
let decompressedBody = response.body;
|
528
|
+
const contentEncodings = contentEncodingHeader.split(',');
|
529
|
+
if (!contentEncodings.every(encoding => getSupportedEncodings(fetchAPI).includes(encoding))) {
|
530
|
+
return new fetchAPI.Response(`Unsupported 'Content-Encoding': ${contentEncodingHeader}`, {
|
531
|
+
status: 415,
|
532
|
+
statusText: 'Unsupported Media Type',
|
533
|
+
});
|
534
|
+
}
|
535
|
+
for (const contentEncoding of contentEncodings) {
|
536
|
+
decompressedBody = decompressedBody.pipeThrough(new fetchAPI.DecompressionStream(contentEncoding));
|
537
|
+
}
|
538
|
+
decompressedResponse = new fetchAPI.Response(decompressedBody, response);
|
539
|
+
decompressedResponseMap.set(response, decompressedResponse);
|
540
|
+
}
|
541
|
+
return decompressedResponse;
|
542
|
+
}
|
package/esm/uwebsockets.js
CHANGED
@@ -5,18 +5,42 @@ export function getRequestFromUWSRequest({ req, res, fetchAPI, signal }) {
|
|
5
5
|
let body;
|
6
6
|
const method = req.getMethod();
|
7
7
|
if (method !== 'get' && method !== 'head') {
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
8
|
+
let controller;
|
9
|
+
body = new fetchAPI.ReadableStream({
|
10
|
+
start(c) {
|
11
|
+
controller = c;
|
12
|
+
},
|
12
13
|
});
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
if (isLast) {
|
14
|
+
const readable = body.readable;
|
15
|
+
if (readable) {
|
16
|
+
signal.addEventListener('abort', () => {
|
17
17
|
readable.push(null);
|
18
|
-
}
|
19
|
-
|
18
|
+
});
|
19
|
+
res.onData(function (ab, isLast) {
|
20
|
+
const chunk = Buffer.from(ab, 0, ab.byteLength);
|
21
|
+
readable.push(Buffer.from(chunk));
|
22
|
+
if (isLast) {
|
23
|
+
readable.push(null);
|
24
|
+
}
|
25
|
+
});
|
26
|
+
}
|
27
|
+
else {
|
28
|
+
let closed = false;
|
29
|
+
signal.addEventListener('abort', () => {
|
30
|
+
if (!closed) {
|
31
|
+
closed = true;
|
32
|
+
controller.close();
|
33
|
+
}
|
34
|
+
});
|
35
|
+
res.onData(function (ab, isLast) {
|
36
|
+
const chunk = Buffer.from(ab, 0, ab.byteLength);
|
37
|
+
controller.enqueue(Buffer.from(chunk));
|
38
|
+
if (isLast) {
|
39
|
+
closed = true;
|
40
|
+
controller.close();
|
41
|
+
}
|
42
|
+
});
|
43
|
+
}
|
20
44
|
}
|
21
45
|
const headers = new fetchAPI.Headers();
|
22
46
|
req.forEach((key, value) => {
|
@@ -32,6 +56,9 @@ export function getRequestFromUWSRequest({ req, res, fetchAPI, signal }) {
|
|
32
56
|
headers,
|
33
57
|
body: body,
|
34
58
|
signal,
|
59
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
60
|
+
// @ts-ignore - not in the TS types yet
|
61
|
+
duplex: 'half',
|
35
62
|
});
|
36
63
|
}
|
37
64
|
async function forwardResponseBodyToUWSResponse(uwsResponse, fetchResponse, signal) {
|
package/package.json
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
{
|
2
2
|
"name": "@whatwg-node/server",
|
3
|
-
"version": "0.10.0-alpha-
|
3
|
+
"version": "0.10.0-alpha-20240726141316-c6ce93b3598457ebe73b3b725986723af8f5e609",
|
4
4
|
"description": "Fetch API compliant HTTP Server adapter",
|
5
5
|
"sideEffects": false,
|
6
6
|
"dependencies": {
|
7
|
-
"@whatwg-node/fetch": "^0.9.
|
7
|
+
"@whatwg-node/fetch": "^0.9.19",
|
8
8
|
"tslib": "^2.6.3"
|
9
9
|
},
|
10
10
|
"repository": {
|
package/typings/index.d.cts
CHANGED
@@ -4,5 +4,6 @@ export * from './utils.cjs';
|
|
4
4
|
export * from './plugins/types.cjs';
|
5
5
|
export * from './plugins/useCors.cjs';
|
6
6
|
export * from './plugins/useErrorHandling.cjs';
|
7
|
+
export * from './plugins/useContentEncoding.cjs';
|
7
8
|
export * from './uwebsockets.cjs';
|
8
9
|
export { Response } from '@whatwg-node/fetch';
|
package/typings/index.d.ts
CHANGED
@@ -4,5 +4,6 @@ export * from './utils.js';
|
|
4
4
|
export * from './plugins/types.js';
|
5
5
|
export * from './plugins/useCors.js';
|
6
6
|
export * from './plugins/useErrorHandling.js';
|
7
|
+
export * from './plugins/useContentEncoding.js';
|
7
8
|
export * from './uwebsockets.js';
|
8
9
|
export { Response } from '@whatwg-node/fetch';
|
@@ -6,7 +6,8 @@ export interface ServerAdapterPlugin<TServerContext = {}> {
|
|
6
6
|
export type OnRequestHook<TServerContext> = (payload: OnRequestEventPayload<TServerContext>) => Promise<void> | void;
|
7
7
|
export interface OnRequestEventPayload<TServerContext> {
|
8
8
|
request: Request;
|
9
|
-
|
9
|
+
setRequest(newRequest: Request): void;
|
10
|
+
serverContext: TServerContext;
|
10
11
|
fetchAPI: FetchAPI;
|
11
12
|
requestHandler: ServerAdapterRequestHandler<TServerContext>;
|
12
13
|
setRequestHandler(newRequestHandler: ServerAdapterRequestHandler<TServerContext>): void;
|
@@ -16,6 +17,8 @@ export interface OnRequestEventPayload<TServerContext> {
|
|
16
17
|
export type OnResponseHook<TServerContext> = (payload: OnResponseEventPayload<TServerContext>) => Promise<void> | void;
|
17
18
|
export interface OnResponseEventPayload<TServerContext> {
|
18
19
|
request: Request;
|
19
|
-
serverContext: TServerContext
|
20
|
+
serverContext: TServerContext;
|
20
21
|
response: Response;
|
22
|
+
setResponse(newResponse: Response): void;
|
23
|
+
fetchAPI: FetchAPI;
|
21
24
|
}
|
@@ -6,7 +6,8 @@ export interface ServerAdapterPlugin<TServerContext = {}> {
|
|
6
6
|
export type OnRequestHook<TServerContext> = (payload: OnRequestEventPayload<TServerContext>) => Promise<void> | void;
|
7
7
|
export interface OnRequestEventPayload<TServerContext> {
|
8
8
|
request: Request;
|
9
|
-
|
9
|
+
setRequest(newRequest: Request): void;
|
10
|
+
serverContext: TServerContext;
|
10
11
|
fetchAPI: FetchAPI;
|
11
12
|
requestHandler: ServerAdapterRequestHandler<TServerContext>;
|
12
13
|
setRequestHandler(newRequestHandler: ServerAdapterRequestHandler<TServerContext>): void;
|
@@ -16,6 +17,8 @@ export interface OnRequestEventPayload<TServerContext> {
|
|
16
17
|
export type OnResponseHook<TServerContext> = (payload: OnResponseEventPayload<TServerContext>) => Promise<void> | void;
|
17
18
|
export interface OnResponseEventPayload<TServerContext> {
|
18
19
|
request: Request;
|
19
|
-
serverContext: TServerContext
|
20
|
+
serverContext: TServerContext;
|
20
21
|
response: Response;
|
22
|
+
setResponse(newResponse: Response): void;
|
23
|
+
fetchAPI: FetchAPI;
|
21
24
|
}
|
@@ -4,9 +4,9 @@ export declare class HTTPError extends Error {
|
|
4
4
|
status: number;
|
5
5
|
message: string;
|
6
6
|
headers: HeadersInit;
|
7
|
-
details?: any
|
7
|
+
details?: any;
|
8
8
|
name: string;
|
9
|
-
constructor(status: number, message: string, headers?: HeadersInit, details?: any
|
9
|
+
constructor(status: number, message: string, headers?: HeadersInit, details?: any);
|
10
10
|
}
|
11
11
|
export type ErrorHandler<TServerContext> = (e: any, request: Request, ctx: TServerContext) => Response | Promise<Response>;
|
12
12
|
export declare function useErrorHandling<TServerContext>(onError?: ErrorHandler<TServerContext>): ServerAdapterPlugin<TServerContext>;
|
@@ -4,9 +4,9 @@ export declare class HTTPError extends Error {
|
|
4
4
|
status: number;
|
5
5
|
message: string;
|
6
6
|
headers: HeadersInit;
|
7
|
-
details?: any
|
7
|
+
details?: any;
|
8
8
|
name: string;
|
9
|
-
constructor(status: number, message: string, headers?: HeadersInit, details?: any
|
9
|
+
constructor(status: number, message: string, headers?: HeadersInit, details?: any);
|
10
10
|
}
|
11
11
|
export type ErrorHandler<TServerContext> = (e: any, request: Request, ctx: TServerContext) => Response | Promise<Response>;
|
12
12
|
export declare function useErrorHandling<TServerContext>(onError?: ErrorHandler<TServerContext>): ServerAdapterPlugin<TServerContext>;
|
package/typings/utils.d.cts
CHANGED
@@ -2,7 +2,7 @@ import type { IncomingMessage, ServerResponse } from 'http';
|
|
2
2
|
import type { Http2ServerRequest, Http2ServerResponse } from 'http2';
|
3
3
|
import type { Socket } from 'net';
|
4
4
|
import type { Readable } from 'stream';
|
5
|
-
import type { FetchEvent } from './types.cjs';
|
5
|
+
import type { FetchAPI, FetchEvent } from './types.cjs';
|
6
6
|
export declare function isAsyncIterable(body: any): body is AsyncIterable<any>;
|
7
7
|
export interface NodeRequest {
|
8
8
|
protocol?: string;
|
@@ -50,3 +50,6 @@ export interface DeferredPromise<T = void> {
|
|
50
50
|
}
|
51
51
|
export declare function createDeferredPromise<T = void>(): DeferredPromise<T>;
|
52
52
|
export declare function handleAbortSignalAndPromiseResponse(response$: Promise<Response> | Response, abortSignal?: AbortSignal | null): Response | Promise<Response>;
|
53
|
+
export declare const decompressedResponseMap: WeakMap<Response, Response>;
|
54
|
+
export declare function getSupportedEncodings(fetchAPI: FetchAPI): CompressionFormat[];
|
55
|
+
export declare function handleResponseDecompression(response: Response, fetchAPI: FetchAPI): Response;
|
package/typings/utils.d.ts
CHANGED
@@ -2,7 +2,7 @@ import type { IncomingMessage, ServerResponse } from 'http';
|
|
2
2
|
import type { Http2ServerRequest, Http2ServerResponse } from 'http2';
|
3
3
|
import type { Socket } from 'net';
|
4
4
|
import type { Readable } from 'stream';
|
5
|
-
import type { FetchEvent } from './types.js';
|
5
|
+
import type { FetchAPI, FetchEvent } from './types.js';
|
6
6
|
export declare function isAsyncIterable(body: any): body is AsyncIterable<any>;
|
7
7
|
export interface NodeRequest {
|
8
8
|
protocol?: string;
|
@@ -50,3 +50,6 @@ export interface DeferredPromise<T = void> {
|
|
50
50
|
}
|
51
51
|
export declare function createDeferredPromise<T = void>(): DeferredPromise<T>;
|
52
52
|
export declare function handleAbortSignalAndPromiseResponse(response$: Promise<Response> | Response, abortSignal?: AbortSignal | null): Response | Promise<Response>;
|
53
|
+
export declare const decompressedResponseMap: WeakMap<Response, Response>;
|
54
|
+
export declare function getSupportedEncodings(fetchAPI: FetchAPI): CompressionFormat[];
|
55
|
+
export declare function handleResponseDecompression(response: Response, fetchAPI: FetchAPI): Response;
|