@jsenv/server 17.6.0 → 17.6.2

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.
Files changed (54) hide show
  1. package/README.md +47 -48
  2. package/dist/client/default_body_4xx_5xx/4xx.html +1 -1
  3. package/dist/jsenv_server.js +1332 -1269
  4. package/dist/ws/ws.js +3 -1
  5. package/package.json +5 -5
  6. package/src/content_negotiation/pick_content_encoding.js +11 -1
  7. package/src/content_negotiation/pick_content_language.js +11 -1
  8. package/src/content_negotiation/pick_content_type.js +11 -1
  9. package/src/content_negotiation/pick_content_version.js +1 -1
  10. package/src/interfacing_with_node/from_node_request.js +154 -178
  11. package/src/interfacing_with_node/observable_from_file_handle.js +1 -18
  12. package/src/interfacing_with_node/observable_from_node_stream.js +29 -65
  13. package/src/interfacing_with_node/write_node_response.js +59 -103
  14. package/src/internal/allowed_hosts.js +85 -0
  15. package/src/internal/escape_html.js +11 -0
  16. package/src/internal/{headersFromObject.js → headers_from_object.js} +2 -2
  17. package/src/internal/listen.js +32 -5
  18. package/src/internal/{listenRequest.js → listen_request.js} +1 -1
  19. package/src/internal/{listenServerConnectionError.js → listen_server_connection_error.js} +1 -1
  20. package/src/internal/{multiple-header.js → multiple_header.js} +32 -10
  21. package/src/internal/reason_phrase.js +17 -0
  22. package/src/internal/response_composition.js +11 -2
  23. package/src/internal/secure_server.js +22 -0
  24. package/src/internal/server_origins.js +78 -0
  25. package/src/internal/{server-polyglot.js → server_polyglot.js} +13 -38
  26. package/src/internal/track_server_pending_connections.js +50 -0
  27. package/src/internal/track_server_pending_requests.js +47 -0
  28. package/src/plugins/cors/server_plugin_cors.js +39 -6
  29. package/src/plugins/default_body_4xx_5xx/server_plugin_default_body_4xx_5xx.js +37 -29
  30. package/src/plugins/error_handler/server_plugin_error_handler.js +26 -10
  31. package/src/plugins/filesystem/fetch_directory.js +16 -3
  32. package/src/plugins/filesystem/fetch_file.js +80 -21
  33. package/src/plugins/filesystem/filesystem_error_to_response.js +12 -6
  34. package/src/plugins/request_aliases/server_plugin_request_aliases.js +8 -0
  35. package/src/plugins/response_acceptance_check/server_plugin_response_acceptance_check.js +11 -6
  36. package/src/plugins/route_inspector/server_plugin_route_inspector.js +11 -5
  37. package/src/plugins/server_plugins_controller.js +27 -13
  38. package/src/progressive_response.js +24 -0
  39. package/src/router/permissions.js +52 -0
  40. package/src/router/router.js +14 -79
  41. package/src/server_events.js +3 -1
  42. package/src/server_timing/timing_header.js +1 -1
  43. package/src/start_server.js +266 -416
  44. package/src/internal/headersFromString.js +0 -21
  45. package/src/internal/headersToObject.js +0 -7
  46. package/src/internal/headersToString.js +0 -16
  47. package/src/internal/listenClientError.js +0 -27
  48. package/src/internal/trackServerPendingConnections.js +0 -145
  49. package/src/internal/trackServerPendingRequests.js +0 -68
  50. package/src/request_body_handling.js +0 -22
  51. /package/src/internal/{colorizeResponseStatus.js → colorize_response_status.js} +0 -0
  52. /package/src/internal/{listenEvent.js → listen_event.js} +0 -0
  53. /package/src/internal/{normalizeHeaderName.js → normalize_header_name.js} +0 -0
  54. /package/src/internal/{normalizeHeaderValue.js → normalize_header_value.js} +0 -0
package/dist/ws/ws.js CHANGED
@@ -418,7 +418,9 @@ function requirePermessageDeflate () {
418
418
  (typeof opts.serverMaxWindowBits === 'number' &&
419
419
  opts.serverMaxWindowBits > params.server_max_window_bits))) ||
420
420
  (typeof opts.clientMaxWindowBits === 'number' &&
421
- !params.client_max_window_bits)
421
+ (typeof params.client_max_window_bits === 'number'
422
+ ? opts.clientMaxWindowBits > params.client_max_window_bits
423
+ : !params.client_max_window_bits))
422
424
  ) {
423
425
  return false;
424
426
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/server",
3
- "version": "17.6.0",
3
+ "version": "17.6.2",
4
4
  "type": "module",
5
5
  "description": "A modern Node.js HTTP server with declarative routing, content negotiation, and WebSocket support",
6
6
  "repository": {
@@ -54,11 +54,11 @@
54
54
  "@jsenv/url-pattern": "../../tooling/url-pattern",
55
55
  "@jsenv/urls": "../../tooling/urls",
56
56
  "@jsenv/utils": "../../tooling/utils",
57
- "eventsource": "4.1.0",
57
+ "eventsource": "5.1.1",
58
58
  "formidable": "3.5.4",
59
- "playwright": "1.61.1",
60
- "undici": "8.8.0",
61
- "ws": "8.21.1"
59
+ "playwright": "1.62.1",
60
+ "undici": "8.10.0",
61
+ "ws": "8.21.3"
62
62
  },
63
63
  "publishConfig": {
64
64
  "access": "public"
@@ -1,6 +1,16 @@
1
- import { parseMultipleHeader } from "../internal/multiple-header.js";
1
+ import { parseMultipleHeader } from "../internal/multiple_header.js";
2
2
  import { pickAcceptedContent } from "./pick_accepted_content.js";
3
3
 
4
+ /**
5
+ * Pick, among the encodings the server can produce, the one the request
6
+ * prefers according to its `accept-encoding` header (`br` and `brotli` are
7
+ * synonyms).
8
+ *
9
+ * @param {{ headers: Object }} request - Anything carrying lowercased `headers`.
10
+ * @param {Array<string>} availableEncodings - In order of server preference: on a tie the first one wins.
11
+ * @returns {string|null} The encoding to respond with, `null` when the request
12
+ * accepts none of them or has no `accept-encoding` header.
13
+ */
4
14
  export const pickContentEncoding = (request, availableEncodings) => {
5
15
  const { headers = {} } = request;
6
16
  const requestAcceptEncodingHeader = headers["accept-encoding"];
@@ -1,6 +1,16 @@
1
- import { parseMultipleHeader } from "../internal/multiple-header.js";
1
+ import { parseMultipleHeader } from "../internal/multiple_header.js";
2
2
  import { pickAcceptedContent } from "./pick_accepted_content.js";
3
3
 
4
+ /**
5
+ * Pick, among the languages the server can produce, the one the request
6
+ * prefers according to its `accept-language` header. An exact match
7
+ * (`fr-FR`) scores higher than a primary language match (`fr` for `fr-CA`).
8
+ *
9
+ * @param {{ headers: Object }} request - Anything carrying lowercased `headers`.
10
+ * @param {Array<string>} availableLanguages - In order of server preference: on a tie the first one wins.
11
+ * @returns {string|null} The language to respond with, `null` when the request
12
+ * accepts none of them or has no `accept-language` header.
13
+ */
4
14
  export const pickContentLanguage = (request, availableLanguages) => {
5
15
  const { headers = {} } = request;
6
16
  const requestAcceptLanguageHeader = headers["accept-language"];
@@ -1,6 +1,16 @@
1
- import { parseMultipleHeader } from "../internal/multiple-header.js";
1
+ import { parseMultipleHeader } from "../internal/multiple_header.js";
2
2
  import { pickAcceptedContent } from "./pick_accepted_content.js";
3
3
 
4
+ /**
5
+ * Pick, among the media types the server can produce, the one the request
6
+ * prefers according to its `accept` header (quality values honored, `text/*`
7
+ * style wildcards understood).
8
+ *
9
+ * @param {{ headers: Object }} request - Anything carrying lowercased `headers`.
10
+ * @param {Array<string>} availableContentTypes - In order of server preference: on a tie the first one wins.
11
+ * @returns {string|null} The media type to respond with, `null` when the request
12
+ * accepts none of them or has no `accept` header.
13
+ */
4
14
  export const pickContentType = (request, availableContentTypes) => {
5
15
  const { headers = {} } = request;
6
16
  const requestAcceptHeader = headers.accept;
@@ -1,4 +1,4 @@
1
- import { parseMultipleHeader } from "../internal/multiple-header.js";
1
+ import { parseMultipleHeader } from "../internal/multiple_header.js";
2
2
  import { pickAcceptedContent } from "./pick_accepted_content.js";
3
3
 
4
4
  export const pickContentVersion = (request, availableVersions) => {
@@ -1,3 +1,15 @@
1
+ /*
2
+ * Builds the `request` object handed to routes from a node request (http or
3
+ * http2 compat api). It is frozen: a plugin wanting another request returns
4
+ * new properties from "redirectRequest" and gets a copy (see
5
+ * applyRedirectionToRequest). The shape is described in
6
+ * docs/handling_requests.md.
7
+ *
8
+ * The values read from headers (`forwarded`, `cookie`...) come from the
9
+ * network: the parsers must never throw on garbage, a request that cannot
10
+ * be read would otherwise escape the request handler.
11
+ */
12
+
1
13
  import { Abort } from "@jsenv/abort";
2
14
  import { createDetailedMessage } from "@jsenv/humanize";
3
15
  import { CONTENT_TYPE } from "@jsenv/utils/src/content_type/content_type.js";
@@ -5,14 +17,14 @@ import { parse } from "node:querystring";
5
17
  import {
6
18
  colorizeResponseStatus,
7
19
  statusToType,
8
- } from "../internal/colorizeResponseStatus.js";
9
- import { headersFromObject } from "../internal/headersFromObject.js";
10
- import { parseSingleHeaderWithAttributes } from "../internal/multiple-header.js";
20
+ } from "../internal/colorize_response_status.js";
21
+ import { headersFromObject } from "../internal/headers_from_object.js";
22
+ import { parseSingleHeaderWithAttributes } from "../internal/multiple_header.js";
11
23
  import { observableFromNodeStream } from "./observable_from_node_stream.js";
12
24
 
13
25
  export const fromNodeRequest = (
14
26
  nodeRequest,
15
- { serverOrigin, signal, requestBodyLifetime, logger, nagle },
27
+ { serverOrigin, signal, logger, requestBodyMaxSize = Infinity },
16
28
  ) => {
17
29
  const requestLogger = createRequestLogger(nodeRequest, (type, value) => {
18
30
  const logFunction = logger[type];
@@ -52,12 +64,7 @@ export const fromNodeRequest = (
52
64
  // Without this the request body readable stream
53
65
  // might be closed when we'll try to attach "data" and "end" listeners to it
54
66
  nodeRequest.pause();
55
- if (!nagle) {
56
- nodeRequest.connection.setNoDelay(true);
57
- }
58
- const body = observableFromNodeStream(nodeRequest, {
59
- readableLifetime: requestBodyLifetime,
60
- });
67
+ const body = observableFromNodeStream(nodeRequest);
61
68
 
62
69
  let requestOrigin;
63
70
  if (nodeRequest.upgrade) {
@@ -76,15 +83,15 @@ export const fromNodeRequest = (
76
83
 
77
84
  // check the following parsers if we want to support more request body content types
78
85
  // https://github.com/node-formidable/formidable/tree/master/src/parsers
79
- const buffer = async () => {
86
+ // Every reader below buffers the body in memory: past maxSize it throws an
87
+ // error the server answers with 413 (see RequestBodyTooLargeError).
88
+ const buffer = async ({ maxSize = requestBodyMaxSize } = {}) => {
80
89
  // here we don't really need to warn, one might want to read anything as binary
81
- // const contentType = headers["content-type"];
82
- // if (!CONTENT_TYPE.isBinary(contentType)) {
83
- // console.warn(
84
- // `buffer() called on a request with content-type: "${contentType}". A binary content-type was expected.`,
85
- // );
86
- // }
87
- const requestBodyBuffer = await readBody(body, { as: "buffer" });
90
+ const requestBodyBuffer = await readBody(body, {
91
+ as: "buffer",
92
+ maxSize,
93
+ headers,
94
+ });
88
95
  return requestBodyBuffer;
89
96
  };
90
97
  // maybe we could use https://github.com/form-data/form-data
@@ -98,40 +105,52 @@ export const fromNodeRequest = (
98
105
  }
99
106
  const { formidable } = await import("formidable");
100
107
  const form = formidable({});
101
- nodeRequest.resume(); // was paused in line #53
108
+ nodeRequest.resume(); // paused above, formidable reads the node stream directly
102
109
  const [fields, files] = await form.parse(nodeRequest);
103
110
  const requestBodyFormData = { fields, files };
104
111
  return requestBodyFormData;
105
112
  };
106
- const text = async () => {
113
+ const text = async ({ maxSize = requestBodyMaxSize } = {}) => {
107
114
  const contentType = headers["content-type"];
108
115
  if (!CONTENT_TYPE.isTextual(contentType)) {
109
116
  console.warn(
110
117
  `text() called on a request with content-type "${contentType}". A textual content-type was expected.`,
111
118
  );
112
119
  }
113
- const requestBodyString = await readBody(body, { as: "string" });
120
+ const requestBodyString = await readBody(body, {
121
+ as: "string",
122
+ maxSize,
123
+ headers,
124
+ });
114
125
  return requestBodyString;
115
126
  };
116
- const json = async () => {
127
+ const json = async ({ maxSize = requestBodyMaxSize } = {}) => {
117
128
  const contentType = headers["content-type"];
118
129
  if (!CONTENT_TYPE.isJson(contentType)) {
119
130
  console.warn(
120
131
  `json() called on a request with content-type "${contentType}". A json content-type was expected.`,
121
132
  );
122
133
  }
123
- const requestBodyString = await readBody(body, { as: "string" });
134
+ const requestBodyString = await readBody(body, {
135
+ as: "string",
136
+ maxSize,
137
+ headers,
138
+ });
124
139
  const requestBodyJSON = JSON.parse(requestBodyString);
125
140
  return requestBodyJSON;
126
141
  };
127
- const queryString = async () => {
142
+ const queryString = async ({ maxSize = requestBodyMaxSize } = {}) => {
128
143
  const contentType = headers["content-type"];
129
144
  if (contentType !== "application/x-www-form-urlencoded") {
130
145
  console.warn(
131
146
  `queryString() called on a request with content-type "${contentType}". application/x-www-form-urlencoded was expected.`,
132
147
  );
133
148
  }
134
- const requestBodyString = await readBody(body, { as: "string" });
149
+ const requestBodyString = await readBody(body, {
150
+ as: "string",
151
+ maxSize,
152
+ headers,
153
+ });
135
154
  const requestBodyQueryStringParsed = parse(requestBodyString);
136
155
  return requestBodyQueryStringParsed;
137
156
  };
@@ -139,6 +158,7 @@ export const fromNodeRequest = (
139
158
  // request.ip -> request ip as received by the server
140
159
  // request.ipForwarded -> ip of the client before proxying, undefined when there is no proxy
141
160
  // same applies on request.proto and request.host
161
+ // These forwarded values are what the headers say: any client can send them.
142
162
  let ip = nodeRequest.socket.remoteAddress;
143
163
  let proto = requestOrigin.startsWith("http:") ? "http" : "https";
144
164
  let host = headers["host"];
@@ -157,7 +177,7 @@ export const fromNodeRequest = (
157
177
  const forwardedHost = headers["x-forwarded-host"];
158
178
  if (forwardedFor) {
159
179
  // format is <client-ip>, <proxy1>, <proxy2>
160
- ipForwarded = forwardedFor.split(",")[0];
180
+ ipForwarded = forwardedFor.split(",")[0].trim();
161
181
  }
162
182
  if (forwardedProto) {
163
183
  protoForwarded = forwardedProto;
@@ -195,52 +215,27 @@ export const fromNodeRequest = (
195
215
  });
196
216
  };
197
217
 
218
+ // Handling a request is asynchronous: its logs are buffered until the
219
+ // response headers are sent (or the request is dropped) so that the logs of
220
+ // concurrent requests do not interleave.
198
221
  const createRequestLogger = (nodeRequest, write) => {
199
- // Handling request is asynchronous, we buffer logs for that request
200
- // until we know what happens with that request
201
- // It delays logs until we know of the request will be handled
202
- // but it's mandatory to make logs readable.
203
-
204
222
  const logArray = [];
205
- const childArray = [];
206
- const add = ({ type, value }) => {
223
+ const add = (type, value) => {
207
224
  logArray.push({ type, value });
208
225
  };
209
226
 
210
227
  const requestLogger = {
211
- logArray,
212
- childArray,
213
- hasPushChild: false,
214
- forPush: () => {
215
- const childLogBuffer = createRequestLogger(nodeRequest, write);
216
- childLogBuffer.isChild = true;
217
- childArray.push(childLogBuffer);
218
- requestLogger.hasPushChild = true;
219
- return childLogBuffer;
220
- },
221
228
  debug: (value) => {
222
- add({
223
- type: "debug",
224
- value,
225
- });
229
+ add("debug", value);
226
230
  },
227
231
  info: (value) => {
228
- add({
229
- type: "info",
230
- value,
231
- });
232
+ add("info", value);
232
233
  },
233
234
  warn: (value) => {
234
- add({
235
- type: "warn",
236
- value,
237
- });
235
+ add("warn", value);
238
236
  },
239
237
  error: (value) => {
240
- add({
241
- type: "error",
242
- value,
243
- });
238
+ add("error", value);
244
239
  },
245
240
  onHeadersSent: ({ status, statusText }) => {
246
241
  const isFaviconNotFound =
@@ -256,8 +251,8 @@ const createRequestLogger = (nodeRequest, write) => {
256
251
  if (statusText) {
257
252
  message += ` ${statusText}`;
258
253
  }
259
- add({
260
- type: isFaviconNotFound
254
+ add(
255
+ isFaviconNotFound
261
256
  ? "debug"
262
257
  : {
263
258
  information: "info",
@@ -266,8 +261,8 @@ const createRequestLogger = (nodeRequest, write) => {
266
261
  client_error: "warn",
267
262
  server_error: "error",
268
263
  }[statusType] || "error",
269
- value: message,
270
- });
264
+ message,
265
+ );
271
266
  },
272
267
  ended: false,
273
268
  end: () => {
@@ -275,19 +270,24 @@ const createRequestLogger = (nodeRequest, write) => {
275
270
  return;
276
271
  }
277
272
  requestLogger.ended = true;
278
- if (requestLogger.isChild) {
279
- // keep buffering until root request write logs for everyone
273
+ if (logArray.length === 0) {
280
274
  return;
281
275
  }
282
- const prefixLines = (string, prefix) => {
283
- return string.replace(/^(?!\s*$)/gm, prefix);
284
- };
285
- const writeLog = (
286
- { type, value },
287
- { someLogIsError, someLogIsWarn, depth },
288
- ) => {
289
- if (depth > 0) {
290
- value = prefixLines(value, " ".repeat(depth));
276
+ let someLogIsError = false;
277
+ let someLogIsWarn = false;
278
+ for (const log of logArray) {
279
+ if (log.type === "error") {
280
+ someLogIsError = true;
281
+ }
282
+ if (log.type === "warn") {
283
+ someLogIsWarn = true;
284
+ }
285
+ }
286
+ // every info log of a request that went wrong is written at the
287
+ // warn/error level, so that it shows up next to what went wrong
288
+ const writeLog = ({ type, value }, { indent }) => {
289
+ if (indent) {
290
+ value = prefixLines(value, " ");
291
291
  }
292
292
  if (type === "info") {
293
293
  if (someLogIsError) {
@@ -298,83 +298,86 @@ const createRequestLogger = (nodeRequest, write) => {
298
298
  }
299
299
  write(type, value);
300
300
  };
301
- const writeLogs = (loggerToWrite, depth) => {
302
- const logArray = loggerToWrite.logArray;
303
- if (logArray.length === 0) {
304
- return;
305
- }
306
- let someLogIsError = false;
307
- let someLogIsWarn = false;
308
- for (const log of loggerToWrite.logArray) {
309
- if (log.type === "error") {
310
- someLogIsError = true;
311
- }
312
- if (log.type === "warn") {
313
- someLogIsWarn = true;
314
- }
315
- }
316
- const firstLog = logArray.shift();
317
- const lastLog = logArray.pop();
318
- const middleLogs = logArray;
319
- writeLog(firstLog, {
320
- someLogIsError,
321
- someLogIsWarn,
322
- depth,
323
- });
324
- for (const middleLog of middleLogs) {
325
- writeLog(middleLog, {
326
- someLogIsError,
327
- someLogIsWarn,
328
- depth,
329
- });
330
- }
331
- for (const childLoggerToWrite of loggerToWrite.childArray) {
332
- writeLogs(childLoggerToWrite, depth + 1);
333
- }
334
- if (lastLog) {
335
- writeLog(lastLog, {
336
- someLogIsError,
337
- someLogIsWarn,
338
- depth: depth + 1,
339
- });
340
- }
341
- };
342
- writeLogs(requestLogger, 0);
301
+ // the last log is the response status, shown under the request line
302
+ const lastLog = logArray.length > 1 ? logArray.pop() : null;
303
+ for (const log of logArray) {
304
+ writeLog(log, { indent: false });
305
+ }
306
+ if (lastLog) {
307
+ writeLog(lastLog, { indent: true });
308
+ }
343
309
  },
344
310
  };
345
311
 
346
312
  return requestLogger;
347
313
  };
348
314
 
349
- const readBody = (body, { as }) => {
315
+ const prefixLines = (string, prefix) => {
316
+ return string.replace(/^(?!\s*$)/gm, prefix);
317
+ };
318
+
319
+ const readBody = (body, { as, maxSize, headers }) => {
350
320
  return new Promise((resolve, reject) => {
321
+ const contentLength = headers["content-length"];
322
+ if (contentLength !== undefined && Number(contentLength) > maxSize) {
323
+ reject(new RequestBodyTooLargeError({ maxSize }));
324
+ return;
325
+ }
351
326
  const bufferArray = [];
352
- body.subscribe({
353
- error: reject,
354
- next: (buffer) => {
355
- bufferArray.push(buffer);
356
- },
357
- complete: () => {
358
- const bodyAsBuffer = Buffer.concat(bufferArray);
359
- if (as === "buffer") {
360
- resolve(bodyAsBuffer);
361
- return;
362
- }
363
- if (as === "string") {
364
- const bodyAsString = bodyAsBuffer.toString();
365
- resolve(bodyAsString);
366
- return;
367
- }
368
- if (as === "json") {
369
- const bodyAsString = bodyAsBuffer.toString();
370
- const bodyAsJSON = JSON.parse(bodyAsString);
371
- resolve(bodyAsJSON);
372
- return;
373
- }
327
+ let length = 0;
328
+ const abortController = new AbortController();
329
+ body.subscribe(
330
+ {
331
+ error: reject,
332
+ next: (buffer) => {
333
+ length += buffer.length;
334
+ if (length > maxSize) {
335
+ abortController.abort();
336
+ reject(new RequestBodyTooLargeError({ maxSize }));
337
+ return;
338
+ }
339
+ bufferArray.push(buffer);
340
+ },
341
+ complete: () => {
342
+ const bodyAsBuffer = Buffer.concat(bufferArray);
343
+ if (as === "buffer") {
344
+ resolve(bodyAsBuffer);
345
+ return;
346
+ }
347
+ if (as === "string") {
348
+ const bodyAsString = bodyAsBuffer.toString();
349
+ resolve(bodyAsString);
350
+ return;
351
+ }
352
+ if (as === "json") {
353
+ const bodyAsString = bodyAsBuffer.toString();
354
+ const bodyAsJSON = JSON.parse(bodyAsString);
355
+ resolve(bodyAsJSON);
356
+ return;
357
+ }
358
+ },
374
359
  },
375
- });
360
+ { signal: abortController.signal },
361
+ );
376
362
  });
377
363
  };
364
+
365
+ class RequestBodyTooLargeError extends Error {
366
+ constructor({ maxSize }) {
367
+ super(`request body exceeds ${maxSize} bytes`);
368
+ this.name = "RequestBodyTooLargeError";
369
+ this.code = "REQUEST_BODY_TOO_LARGE";
370
+ this.maxSize = maxSize;
371
+ }
372
+
373
+ asResponse() {
374
+ return {
375
+ status: 413,
376
+ statusText: "Payload Too Large",
377
+ statusMessage: `The request body exceeds ${this.maxSize} bytes.`,
378
+ };
379
+ }
380
+ }
378
381
  // exported for unit tests
379
382
  export const readRequestBody = (request, { as }) => {
380
383
  if (as === "string") {
@@ -400,12 +403,22 @@ const parseRequestCookieHeader = (cookieHeader) => {
400
403
  continue;
401
404
  }
402
405
  const name = pair.slice(0, eqIndex).trim();
403
- const value = decodeURIComponent(pair.slice(eqIndex + 1).trim());
406
+ const value = decodeCookieValue(pair.slice(eqIndex + 1).trim());
404
407
  map.set(name, value);
405
408
  }
406
409
  return map;
407
410
  };
408
411
 
412
+ // a cookie value is not necessarily percent-encoded: a malformed sequence
413
+ // keeps the raw value
414
+ const decodeCookieValue = (value) => {
415
+ try {
416
+ return decodeURIComponent(value);
417
+ } catch {
418
+ return value;
419
+ }
420
+ };
421
+
409
422
  export const applyRedirectionToRequest = (
410
423
  request,
411
424
  { resource, pathname, ...rest },
@@ -443,40 +456,3 @@ const getPropertiesFromPathname = ({ pathname, baseUrl }) => {
443
456
  baseUrl,
444
457
  });
445
458
  };
446
-
447
- export const createPushRequest = (
448
- request,
449
- { signal, pathname, method, logger },
450
- ) => {
451
- const pushRequest = Object.freeze({
452
- ...request,
453
- logger,
454
- parent: request,
455
- signal,
456
- http2: true,
457
- ...(pathname
458
- ? getPropertiesFromPathname({
459
- pathname,
460
- baseUrl: request.url,
461
- })
462
- : {}),
463
- method: method || request.method,
464
- headers: getHeadersInheritedByPushRequest(request),
465
- body: undefined,
466
- });
467
- return pushRequest;
468
- };
469
-
470
- const getHeadersInheritedByPushRequest = (request) => {
471
- const headersInherited = { ...request.headers };
472
- // mtime sent by the client in request headers concerns the main request
473
- // Time remains valid for request to other resources so we keep it
474
- // in child requests
475
- // delete childHeaders["if-modified-since"]
476
-
477
- // eTag sent by the client in request headers concerns the main request
478
- // A request made to an other resource must not inherit the eTag
479
- delete headersInherited["if-none-match"];
480
-
481
- return headersInherited;
482
- };
@@ -1,4 +1,3 @@
1
- import { createReadStream } from "node:fs";
2
1
  import { observableFromNodeStream } from "./observable_from_node_stream.js";
3
2
 
4
3
  export const isFileHandle = (value) => {
@@ -10,21 +9,5 @@ export const observableFromFileHandle = (fileHandle) => {
10
9
  };
11
10
 
12
11
  export const fileHandleToReadableStream = (fileHandle) => {
13
- const fileReadableStream =
14
- typeof fileHandle.createReadStream === "function"
15
- ? fileHandle.createReadStream()
16
- : createReadStream(
17
- "/toto", // is it ok to pass a fake path like this?
18
- {
19
- fd: fileHandle.fd,
20
- emitClose: true,
21
- // autoClose: true
22
- },
23
- );
24
- // I suppose it's required only when doing fs.createReadStream()
25
- // and not fileHandle.createReadStream()
26
- // fileReadableStream.on("end", () => {
27
- // fileHandle.close()
28
- // })
29
- return fileReadableStream;
12
+ return fileHandle.createReadStream();
30
13
  };