@jsenv/server 17.6.1 → 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.
- package/README.md +47 -48
- package/dist/jsenv_server.js +1332 -1269
- package/package.json +2 -2
- package/src/content_negotiation/pick_content_encoding.js +11 -1
- package/src/content_negotiation/pick_content_language.js +11 -1
- package/src/content_negotiation/pick_content_type.js +11 -1
- package/src/content_negotiation/pick_content_version.js +1 -1
- package/src/interfacing_with_node/from_node_request.js +154 -178
- package/src/interfacing_with_node/observable_from_file_handle.js +1 -18
- package/src/interfacing_with_node/observable_from_node_stream.js +29 -65
- package/src/interfacing_with_node/write_node_response.js +59 -103
- package/src/internal/allowed_hosts.js +85 -0
- package/src/internal/escape_html.js +11 -0
- package/src/internal/{headersFromObject.js → headers_from_object.js} +2 -2
- package/src/internal/listen.js +32 -5
- package/src/internal/{listenRequest.js → listen_request.js} +1 -1
- package/src/internal/{listenServerConnectionError.js → listen_server_connection_error.js} +1 -1
- package/src/internal/{multiple-header.js → multiple_header.js} +32 -10
- package/src/internal/reason_phrase.js +17 -0
- package/src/internal/response_composition.js +11 -2
- package/src/internal/secure_server.js +22 -0
- package/src/internal/server_origins.js +78 -0
- package/src/internal/{server-polyglot.js → server_polyglot.js} +13 -38
- package/src/internal/track_server_pending_connections.js +50 -0
- package/src/internal/track_server_pending_requests.js +47 -0
- package/src/plugins/cors/server_plugin_cors.js +39 -6
- package/src/plugins/default_body_4xx_5xx/server_plugin_default_body_4xx_5xx.js +37 -29
- package/src/plugins/error_handler/server_plugin_error_handler.js +26 -10
- package/src/plugins/filesystem/fetch_directory.js +16 -3
- package/src/plugins/filesystem/fetch_file.js +80 -21
- package/src/plugins/filesystem/filesystem_error_to_response.js +12 -6
- package/src/plugins/request_aliases/server_plugin_request_aliases.js +8 -0
- package/src/plugins/response_acceptance_check/server_plugin_response_acceptance_check.js +11 -6
- package/src/plugins/route_inspector/server_plugin_route_inspector.js +11 -5
- package/src/plugins/server_plugins_controller.js +27 -13
- package/src/progressive_response.js +24 -0
- package/src/router/permissions.js +52 -0
- package/src/router/router.js +14 -79
- package/src/server_events.js +3 -1
- package/src/server_timing/timing_header.js +1 -1
- package/src/start_server.js +266 -416
- package/src/internal/headersFromString.js +0 -21
- package/src/internal/headersToObject.js +0 -7
- package/src/internal/headersToString.js +0 -16
- package/src/internal/listenClientError.js +0 -27
- package/src/internal/trackServerPendingConnections.js +0 -145
- package/src/internal/trackServerPendingRequests.js +0 -68
- package/src/request_body_handling.js +0 -22
- /package/src/internal/{colorizeResponseStatus.js → colorize_response_status.js} +0 -0
- /package/src/internal/{listenEvent.js → listen_event.js} +0 -0
- /package/src/internal/{normalizeHeaderName.js → normalize_header_name.js} +0 -0
- /package/src/internal/{normalizeHeaderValue.js → normalize_header_value.js} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsenv/server",
|
|
3
|
-
"version": "17.6.
|
|
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,7 +54,7 @@
|
|
|
54
54
|
"@jsenv/url-pattern": "../../tooling/url-pattern",
|
|
55
55
|
"@jsenv/urls": "../../tooling/urls",
|
|
56
56
|
"@jsenv/utils": "../../tooling/utils",
|
|
57
|
-
"eventsource": "5.1.
|
|
57
|
+
"eventsource": "5.1.1",
|
|
58
58
|
"formidable": "3.5.4",
|
|
59
59
|
"playwright": "1.62.1",
|
|
60
60
|
"undici": "8.10.0",
|
|
@@ -1,6 +1,16 @@
|
|
|
1
|
-
import { parseMultipleHeader } from "../internal/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
9
|
-
import { headersFromObject } from "../internal/
|
|
10
|
-
import { parseSingleHeaderWithAttributes } from "../internal/
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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(); //
|
|
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, {
|
|
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, {
|
|
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, {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
279
|
-
// keep buffering until root request write logs for everyone
|
|
273
|
+
if (logArray.length === 0) {
|
|
280
274
|
return;
|
|
281
275
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
|
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
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
};
|