@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.
@@ -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 (onRequestHooks.length === 0) {
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
- if (nodeResponse.once) {
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: nodeRequest.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: nodeRequest.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: nodeRequest.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: nodeRequest.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: nodeRequest.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
- descriptors[key] = Object.getOwnPropertyDescriptor(source, key);
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.enumerable) {
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
+ }
@@ -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
- body = new fetchAPI.ReadableStream({});
14
- const readable = body.readable;
15
- signal.addEventListener('abort', () => {
16
- readable.push(null);
13
+ let controller;
14
+ body = new fetchAPI.ReadableStream({
15
+ start(c) {
16
+ controller = c;
17
+ },
17
18
  });
18
- res.onData(function (ab, isLast) {
19
- const chunk = Buffer.from(ab, 0, ab.byteLength);
20
- readable.push(Buffer.from(chunk));
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 (onRequestHooks.length === 0) {
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
- if (nodeResponse.once) {
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: nodeRequest.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: nodeRequest.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: nodeRequest.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: nodeRequest.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: nodeRequest.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
- descriptors[key] = Object.getOwnPropertyDescriptor(source, key);
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.enumerable) {
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
+ }
@@ -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
- body = new fetchAPI.ReadableStream({});
9
- const readable = body.readable;
10
- signal.addEventListener('abort', () => {
11
- readable.push(null);
8
+ let controller;
9
+ body = new fetchAPI.ReadableStream({
10
+ start(c) {
11
+ controller = c;
12
+ },
12
13
  });
13
- res.onData(function (ab, isLast) {
14
- const chunk = Buffer.from(ab, 0, ab.byteLength);
15
- readable.push(Buffer.from(chunk));
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-20240717144936-662ea5088eb83ec5b55025307193fac81a8ea190",
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.17",
7
+ "@whatwg-node/fetch": "^0.9.19",
8
8
  "tslib": "^2.6.3"
9
9
  },
10
10
  "repository": {
@@ -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';
@@ -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
- serverContext: TServerContext | undefined;
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 | undefined;
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
- serverContext: TServerContext | undefined;
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 | undefined;
20
+ serverContext: TServerContext;
20
21
  response: Response;
22
+ setResponse(newResponse: Response): void;
23
+ fetchAPI: FetchAPI;
21
24
  }
@@ -0,0 +1,2 @@
1
+ import type { ServerAdapterPlugin } from './types.cjs';
2
+ export declare function useContentEncoding<TServerContext>(): ServerAdapterPlugin<TServerContext>;
@@ -0,0 +1,2 @@
1
+ import type { ServerAdapterPlugin } from './types.js';
2
+ export declare function useContentEncoding<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 | undefined;
7
+ details?: any;
8
8
  name: string;
9
- constructor(status: number, message: string, headers?: HeadersInit, details?: any | undefined);
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 | undefined;
7
+ details?: any;
8
8
  name: string;
9
- constructor(status: number, message: string, headers?: HeadersInit, details?: any | undefined);
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>;
@@ -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;
@@ -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;