@jsenv/server 7.1.0 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -23
- package/main.js +32 -17
- package/package.json +20 -15
- package/src/{internal → content_negotiation}/applyContentNegotiation.js +5 -1
- package/src/{internal → content_negotiation}/checkContentNegotiation.js +3 -3
- package/src/{negotiateContentEncoding.js → content_negotiation/negotiateContentEncoding.js} +5 -3
- package/src/{negotiateContentLanguage.js → content_negotiation/negotiateContentLanguage.js} +9 -5
- package/src/{negotiateContentType.js → content_negotiation/negotiateContentType.js} +10 -5
- package/src/content_negotiation/plugin_content_negotiation_check.js +15 -0
- package/src/cors/plugin_cors.js +156 -0
- package/src/fetchUrl.js +12 -7
- package/src/findFreePort.js +2 -1
- package/src/{composeResponse.js → internal/composeResponse.js} +5 -3
- package/src/internal/compositionMappingToCompose.js +26 -6
- package/src/{convertFileSystemErrorToResponseProperties.js → internal/convertFileSystemErrorToResponseProperties.js} +0 -0
- package/src/internal/getServerOrigins.js +5 -1
- package/src/internal/headersFromObject.js +3 -1
- package/src/{headersToObject.js → internal/headersToObject.js} +0 -0
- package/src/internal/listenClientError.js +5 -1
- package/src/internal/listenEvent.js +6 -1
- package/src/internal/listenRequest.js +5 -1
- package/src/internal/listenServerConnectionError.js +31 -23
- package/src/internal/multiple-header.js +11 -5
- package/src/internal/nodeRequestToRequest.js +10 -7
- package/src/internal/observable.js +5 -1
- package/src/internal/populateNodeResponse.js +11 -2
- package/src/internal/server-polyglot.js +11 -2
- package/src/internal/trackServerPendingConnections.js +15 -11
- package/src/internal/trackServerPendingRequests.js +10 -7
- package/src/jsenvServerInternalErrorToResponse.js +7 -3
- package/src/plugin_request_waiting_check.js +34 -0
- package/src/serveFile.js +88 -41
- package/src/server_timing/plugin_server_timing.js +28 -0
- package/src/{serverTiming.js → server_timing/timing_header.js} +43 -62
- package/src/server_timing/timing_measure.js +26 -0
- package/src/service_composition/service_composition.js +65 -0
- package/src/{createSSERoom.js → sse/createSSERoom.js} +22 -6
- package/src/startServer.js +197 -252
- package/src/stopReasons.js +3 -1
- package/src/urlToContentType.js +13 -6
- package/src/jsenvAccessControlAllowedHeaders.js +0 -1
- package/src/jsenvAccessControlAllowedMethods.js +0 -1
- package/src/service-composition.js +0 -41
- package/src/urlToSearchParamValue.js +0 -3
package/src/startServer.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
/* eslint-disable import/max-dependencies */
|
|
2
|
-
|
|
3
1
|
import http from "node:http"
|
|
4
2
|
import {
|
|
5
3
|
createCancellationToken,
|
|
@@ -9,21 +7,27 @@ import {
|
|
|
9
7
|
isCancelError,
|
|
10
8
|
executeAsyncFunction,
|
|
11
9
|
} from "@jsenv/cancellation"
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
import {
|
|
11
|
+
SIGINTSignal,
|
|
12
|
+
unadvisedCrashSignal,
|
|
13
|
+
teardownSignal,
|
|
14
|
+
} from "@jsenv/node-signals"
|
|
15
|
+
import { memoize } from "@jsenv/filesystem"
|
|
16
|
+
import { createDetailedMessage, createLogger } from "@jsenv/logger"
|
|
15
17
|
|
|
16
18
|
import { createTracker } from "./internal/createTracker.js"
|
|
17
19
|
import { createPolyglotServer } from "./internal/server-polyglot.js"
|
|
18
20
|
import { trackServerPendingConnections } from "./internal/trackServerPendingConnections.js"
|
|
19
21
|
import { trackServerPendingRequests } from "./internal/trackServerPendingRequests.js"
|
|
20
22
|
import { nodeRequestToRequest } from "./internal/nodeRequestToRequest.js"
|
|
21
|
-
import { composeResponseHeaders } from "./internal/composeResponseHeaders.js"
|
|
22
23
|
import { populateNodeResponse } from "./internal/populateNodeResponse.js"
|
|
23
24
|
import { colorizeResponseStatus } from "./internal/colorizeResponseStatus.js"
|
|
24
25
|
import { getServerOrigins } from "./internal/getServerOrigins.js"
|
|
25
26
|
import { listen, stopListening } from "./internal/listen.js"
|
|
26
|
-
import { composeResponse } from "./composeResponse.js"
|
|
27
|
+
import { composeResponse } from "./internal/composeResponse.js"
|
|
28
|
+
import { listenRequest } from "./internal/listenRequest.js"
|
|
29
|
+
import { listenServerConnectionError } from "./internal/listenServerConnectionError.js"
|
|
30
|
+
import { jsenvServerInternalErrorToResponse } from "./jsenvServerInternalErrorToResponse.js"
|
|
27
31
|
import {
|
|
28
32
|
STOP_REASON_INTERNAL_ERROR,
|
|
29
33
|
STOP_REASON_PROCESS_SIGHUP,
|
|
@@ -33,13 +37,6 @@ import {
|
|
|
33
37
|
STOP_REASON_PROCESS_EXIT,
|
|
34
38
|
STOP_REASON_NOT_SPECIFIED,
|
|
35
39
|
} from "./stopReasons.js"
|
|
36
|
-
import { jsenvAccessControlAllowedHeaders } from "./jsenvAccessControlAllowedHeaders.js"
|
|
37
|
-
import { jsenvAccessControlAllowedMethods } from "./jsenvAccessControlAllowedMethods.js"
|
|
38
|
-
import { listenRequest } from "./internal/listenRequest.js"
|
|
39
|
-
import { timeFunction, timingToServerTimingResponseHeaders } from "./serverTiming.js"
|
|
40
|
-
import { jsenvServerInternalErrorToResponse } from "./jsenvServerInternalErrorToResponse.js"
|
|
41
|
-
import { checkContentNegotiation } from "./internal/checkContentNegotiation.js"
|
|
42
|
-
import { listenServerConnectionError } from "./internal/listenServerConnectionError.js"
|
|
43
40
|
|
|
44
41
|
export const startServer = async ({
|
|
45
42
|
cancellationToken = createCancellationToken(),
|
|
@@ -66,36 +63,11 @@ export const startServer = async ({
|
|
|
66
63
|
stopOnCrash = false,
|
|
67
64
|
keepProcessAlive = true,
|
|
68
65
|
requestToResponse = () => null,
|
|
66
|
+
plugins = {},
|
|
69
67
|
|
|
70
|
-
accessControlAllowedOrigins = [],
|
|
71
|
-
accessControlAllowedMethods = jsenvAccessControlAllowedMethods,
|
|
72
|
-
accessControlAllowedHeaders = jsenvAccessControlAllowedHeaders,
|
|
73
|
-
accessControlAllowRequestOrigin = false,
|
|
74
|
-
accessControlAllowRequestMethod = false,
|
|
75
|
-
accessControlAllowRequestHeaders = false,
|
|
76
|
-
accessControlAllowCredentials = false,
|
|
77
|
-
// by default OPTIONS request can be cache for a long time, it's not going to change soon ?
|
|
78
|
-
// we could put a lot here, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age
|
|
79
|
-
accessControlMaxAge = 600,
|
|
80
|
-
|
|
81
|
-
// https://www.w3.org/TR/server-timing/
|
|
82
|
-
sendServerTiming = false,
|
|
83
68
|
sendServerInternalErrorDetails = false,
|
|
84
69
|
serverInternalErrorToResponse = jsenvServerInternalErrorToResponse,
|
|
85
70
|
|
|
86
|
-
requestWaitingMs = 20000,
|
|
87
|
-
requestWaitingCallback = (request, { logger }) => {
|
|
88
|
-
logger.warn(
|
|
89
|
-
`still no response found for request after ${requestWaitingMs} ms
|
|
90
|
-
--- request url ---
|
|
91
|
-
${request.origin}${request.ressource}
|
|
92
|
-
--- request headers ---
|
|
93
|
-
${JSON.stringify(request.headers, null, " ")}
|
|
94
|
-
`,
|
|
95
|
-
)
|
|
96
|
-
},
|
|
97
|
-
contentNegotiationWarnings = true,
|
|
98
|
-
|
|
99
71
|
startedCallback = () => {},
|
|
100
72
|
stoppedCallback = () => {},
|
|
101
73
|
errorIsCancellation = () => false,
|
|
@@ -118,7 +90,11 @@ ${JSON.stringify(request.headers, null, " ")}
|
|
|
118
90
|
}
|
|
119
91
|
|
|
120
92
|
const logger = createLogger({ logLevel })
|
|
121
|
-
if (
|
|
93
|
+
if (
|
|
94
|
+
redirectHttpToHttps === undefined &&
|
|
95
|
+
protocol === "https" &&
|
|
96
|
+
!allowHttpRequestOnHttps
|
|
97
|
+
) {
|
|
122
98
|
redirectHttpToHttps = true
|
|
123
99
|
}
|
|
124
100
|
if (redirectHttpToHttps && protocol === "http") {
|
|
@@ -126,7 +102,9 @@ ${JSON.stringify(request.headers, null, " ")}
|
|
|
126
102
|
redirectHttpToHttps = false
|
|
127
103
|
}
|
|
128
104
|
if (allowHttpRequestOnHttps && redirectHttpToHttps) {
|
|
129
|
-
logger.warn(
|
|
105
|
+
logger.warn(
|
|
106
|
+
`redirectHttpToHttps ignored because allowHttpRequestOnHttps is enabled`,
|
|
107
|
+
)
|
|
130
108
|
redirectHttpToHttps = false
|
|
131
109
|
}
|
|
132
110
|
|
|
@@ -208,7 +186,10 @@ ${JSON.stringify(request.headers, null, " ")}
|
|
|
208
186
|
const stop = memoize(async (reason = STOP_REASON_NOT_SPECIFIED) => {
|
|
209
187
|
status = "stopping"
|
|
210
188
|
|
|
211
|
-
errorIsCancellation = composePredicate(
|
|
189
|
+
errorIsCancellation = composePredicate(
|
|
190
|
+
errorIsCancellation,
|
|
191
|
+
(error) => error === reason,
|
|
192
|
+
)
|
|
212
193
|
errorIsCancellation = composePredicate(
|
|
213
194
|
errorIsCancellation,
|
|
214
195
|
(error) => error && error.code === "ECONNRESET",
|
|
@@ -241,7 +222,10 @@ ${JSON.stringify(request.headers, null, " ")}
|
|
|
241
222
|
const serverOrigins = getServerOrigins({ protocol, ip, port })
|
|
242
223
|
const serverOrigin = serverOrigins.main
|
|
243
224
|
|
|
244
|
-
const removeConnectionErrorListener = listenServerConnectionError(
|
|
225
|
+
const removeConnectionErrorListener = listenServerConnectionError(
|
|
226
|
+
nodeServer,
|
|
227
|
+
onError,
|
|
228
|
+
)
|
|
245
229
|
registerCleanupCallback(removeConnectionErrorListener)
|
|
246
230
|
|
|
247
231
|
const connectionsTracker = trackServerPendingConnections(nodeServer, {
|
|
@@ -250,7 +234,9 @@ ${JSON.stringify(request.headers, null, " ")}
|
|
|
250
234
|
// opened connection must be shutdown before the close event is emitted
|
|
251
235
|
registerCleanupCallback(connectionsTracker.stop)
|
|
252
236
|
|
|
253
|
-
const pendingRequestsTracker = trackServerPendingRequests(nodeServer, {
|
|
237
|
+
const pendingRequestsTracker = trackServerPendingRequests(nodeServer, {
|
|
238
|
+
http2,
|
|
239
|
+
})
|
|
254
240
|
// ensure pending requests got a response from the server
|
|
255
241
|
registerCleanupCallback((reason) => {
|
|
256
242
|
pendingRequestsTracker.stop({
|
|
@@ -271,74 +257,173 @@ ${JSON.stringify(request.headers, null, " ")}
|
|
|
271
257
|
return
|
|
272
258
|
}
|
|
273
259
|
|
|
274
|
-
const request = nodeRequestToRequest(nodeRequest, {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
--- request ressource ---
|
|
278
|
-
${request.ressource}
|
|
279
|
-
--- error stack ---
|
|
280
|
-
${error.stack}`)
|
|
260
|
+
const request = nodeRequestToRequest(nodeRequest, {
|
|
261
|
+
serverCancellationToken,
|
|
262
|
+
serverOrigin,
|
|
281
263
|
})
|
|
282
264
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
timingToServerTimingResponseHeaders(serverTiming),
|
|
294
|
-
response.headers,
|
|
295
|
-
)
|
|
296
|
-
|
|
297
|
-
if (contentNegotiationWarnings) {
|
|
298
|
-
checkContentNegotiation(request, response, { warn: logger.warn })
|
|
299
|
-
}
|
|
265
|
+
// Handling request is asynchronous, we buffer logs for that request
|
|
266
|
+
// until we know what happens with that request
|
|
267
|
+
// It delays logs until we know of the request will be handled
|
|
268
|
+
// but it's mandatory to make logs readable.
|
|
269
|
+
let requestLogs = []
|
|
270
|
+
const releaseRequestLogs = () => {
|
|
271
|
+
requestLogs.forEach(({ type, value }) => {
|
|
272
|
+
logger[type](value)
|
|
273
|
+
})
|
|
274
|
+
requestLogs.length = 0
|
|
300
275
|
}
|
|
276
|
+
const warn = (value) => {
|
|
277
|
+
requestLogs.push({
|
|
278
|
+
type: "warn",
|
|
279
|
+
value,
|
|
280
|
+
})
|
|
281
|
+
}
|
|
282
|
+
requestLogs.push({
|
|
283
|
+
type: "info",
|
|
284
|
+
value: `${request.method} ${request.origin}${request.ressource}`,
|
|
285
|
+
})
|
|
301
286
|
|
|
302
|
-
|
|
287
|
+
nodeRequest.on("error", (error) => {
|
|
288
|
+
if (error.message === "aborted") {
|
|
289
|
+
requestLogs.push({
|
|
290
|
+
type: "debug",
|
|
291
|
+
value: createDetailedMessage(`request aborted by client`, {
|
|
292
|
+
"error message": error.message,
|
|
293
|
+
}),
|
|
294
|
+
})
|
|
295
|
+
} else {
|
|
296
|
+
requestLogs.push({
|
|
297
|
+
type: "error",
|
|
298
|
+
value: createDetailedMessage(`"error" event emitted on request`, {
|
|
299
|
+
"error stack": error.stack,
|
|
300
|
+
}),
|
|
301
|
+
})
|
|
302
|
+
}
|
|
303
|
+
})
|
|
303
304
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
305
|
+
const requestPlugins = []
|
|
306
|
+
let responseFromShortcircuit
|
|
307
|
+
const shortcircuitResponse = (value) => {
|
|
308
|
+
responseFromShortcircuit = value
|
|
308
309
|
}
|
|
310
|
+
Object.keys(plugins).forEach((pluginName) => {
|
|
311
|
+
const { onRequest } = plugins[pluginName]
|
|
312
|
+
if (onRequest) {
|
|
313
|
+
const returnValue = onRequest(request, { warn, shortcircuitResponse })
|
|
314
|
+
if (returnValue) {
|
|
315
|
+
requestPlugins.push(returnValue)
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
})
|
|
319
|
+
let { error, responseProperties } = responseFromShortcircuit
|
|
320
|
+
? { responseProperties: responseFromShortcircuit }
|
|
321
|
+
: await generateResponseDescription({
|
|
322
|
+
requestToResponse,
|
|
323
|
+
request,
|
|
324
|
+
})
|
|
309
325
|
|
|
310
|
-
if (error && isCancelError(error)
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
326
|
+
if (error && isCancelError(error)) {
|
|
327
|
+
if (internalCancellationToken.cancellationRequested) {
|
|
328
|
+
requestLogs.push({
|
|
329
|
+
type: "info",
|
|
330
|
+
value: `aborted because server closing`,
|
|
331
|
+
})
|
|
332
|
+
releaseRequestLogs()
|
|
333
|
+
nodeResponse.destroy()
|
|
334
|
+
return
|
|
335
|
+
}
|
|
336
|
+
if (request.cancellationToken.cancellationRequested) {
|
|
337
|
+
requestLogs.push({
|
|
338
|
+
type: "info",
|
|
339
|
+
value: `aborted because request aborted by client`,
|
|
340
|
+
})
|
|
341
|
+
releaseRequestLogs()
|
|
342
|
+
nodeResponse.destroy()
|
|
343
|
+
return
|
|
344
|
+
}
|
|
345
|
+
// unknown cancel error, never supposed to happen
|
|
314
346
|
}
|
|
315
347
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
348
|
+
// internal error, create 500 response
|
|
349
|
+
if (error) {
|
|
350
|
+
const propertiesFromError = await serverInternalErrorToResponse(error, {
|
|
351
|
+
request,
|
|
352
|
+
sendServerInternalErrorDetails,
|
|
353
|
+
})
|
|
354
|
+
responseProperties = composeResponse(
|
|
355
|
+
{
|
|
356
|
+
status: 500,
|
|
357
|
+
statusText: statusToStatusText(500),
|
|
358
|
+
headers: {
|
|
359
|
+
// ensure error are not cached
|
|
360
|
+
"cache-control": "no-store",
|
|
361
|
+
"content-type": "text/plain",
|
|
362
|
+
},
|
|
363
|
+
body: "",
|
|
364
|
+
},
|
|
365
|
+
propertiesFromError,
|
|
366
|
+
)
|
|
367
|
+
requestLogs.push({
|
|
368
|
+
type: "error",
|
|
369
|
+
value: createDetailedMessage(
|
|
370
|
+
`internal error while handling request`,
|
|
371
|
+
{
|
|
372
|
+
"error stack": error.stack,
|
|
373
|
+
},
|
|
374
|
+
),
|
|
375
|
+
})
|
|
376
|
+
} else {
|
|
377
|
+
const {
|
|
378
|
+
status = 501,
|
|
379
|
+
statusText = statusToStatusText(status),
|
|
380
|
+
headers = {},
|
|
381
|
+
body = "",
|
|
382
|
+
...rest
|
|
383
|
+
} = responseProperties || {}
|
|
384
|
+
responseProperties = {
|
|
385
|
+
status,
|
|
386
|
+
statusText,
|
|
387
|
+
headers,
|
|
388
|
+
body,
|
|
389
|
+
...rest,
|
|
390
|
+
}
|
|
320
391
|
}
|
|
321
392
|
|
|
393
|
+
// call every plugin "onResponse" hook
|
|
394
|
+
requestPlugins.forEach((requestPlugin) => {
|
|
395
|
+
const { onResponse } = requestPlugin
|
|
396
|
+
if (onResponse) {
|
|
397
|
+
const returnValue = onResponse(responseProperties)
|
|
398
|
+
if (returnValue) {
|
|
399
|
+
responseProperties = composeResponse(
|
|
400
|
+
responseProperties,
|
|
401
|
+
returnValue,
|
|
402
|
+
)
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
})
|
|
406
|
+
|
|
322
407
|
if (
|
|
323
408
|
request.method !== "HEAD" &&
|
|
324
|
-
|
|
325
|
-
|
|
409
|
+
responseProperties.headers["content-length"] > 0 &&
|
|
410
|
+
responseProperties.body === ""
|
|
326
411
|
) {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
412
|
+
requestLogs.push({
|
|
413
|
+
type: "warn",
|
|
414
|
+
value: `content-length header is ${responseProperties.headers["content-length"]} but body is empty`,
|
|
415
|
+
})
|
|
330
416
|
}
|
|
331
417
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
logger.info(`${colorizeResponseStatus(response.status)} ${response.statusText}`)
|
|
418
|
+
requestLogs.push({
|
|
419
|
+
type: "info",
|
|
420
|
+
value: `${colorizeResponseStatus(responseProperties.status)} ${
|
|
421
|
+
responseProperties.statusText
|
|
422
|
+
}`,
|
|
423
|
+
})
|
|
424
|
+
releaseRequestLogs()
|
|
340
425
|
|
|
341
|
-
populateNodeResponse(nodeResponse,
|
|
426
|
+
populateNodeResponse(nodeResponse, responseProperties, {
|
|
342
427
|
cancellationToken: request.cancellationToken,
|
|
343
428
|
ignoreBody: request.method === "HEAD",
|
|
344
429
|
// https://github.com/nodejs/node/blob/79296dc2d02c0b9872bbfcbb89148ea036a546d0/lib/internal/http2/compat.js#L97
|
|
@@ -348,12 +433,12 @@ ${request.method} ${request.origin}${request.ressource}`)
|
|
|
348
433
|
})
|
|
349
434
|
|
|
350
435
|
if (
|
|
351
|
-
|
|
436
|
+
error &&
|
|
352
437
|
// stopOnInternalError stops server only if requestToResponse generated
|
|
353
438
|
// a non controlled error (internal error).
|
|
354
439
|
// if requestToResponse gracefully produced a 500 response (it did not throw)
|
|
355
440
|
// then we can assume we are still in control of what we are doing
|
|
356
|
-
|
|
441
|
+
stopOnInternalError
|
|
357
442
|
) {
|
|
358
443
|
// il faudrais pouvoir stop que les autres response ?
|
|
359
444
|
stop(STOP_REASON_INTERNAL_ERROR)
|
|
@@ -364,100 +449,11 @@ ${request.method} ${request.origin}${request.ressource}`)
|
|
|
364
449
|
// ensure we don't try to handle new requests while server is stopping
|
|
365
450
|
registerCleanupCallback(removeRequestListener)
|
|
366
451
|
|
|
367
|
-
logger.info(
|
|
452
|
+
logger.info(
|
|
453
|
+
`${serverName} started at ${serverOrigin} (${serverOrigins.external})`,
|
|
454
|
+
)
|
|
368
455
|
startedCallback({ origin: serverOrigin })
|
|
369
456
|
|
|
370
|
-
const corsEnabled = accessControlAllowRequestOrigin || accessControlAllowedOrigins.length
|
|
371
|
-
// here we check access control options to throw or warn if we find strange values
|
|
372
|
-
|
|
373
|
-
const generateResponseDescription = async (request) => {
|
|
374
|
-
const responsePropertiesToResponse = ({
|
|
375
|
-
status = 501,
|
|
376
|
-
statusText = statusToStatusText(status),
|
|
377
|
-
headers = {},
|
|
378
|
-
body = "",
|
|
379
|
-
bodyEncoding,
|
|
380
|
-
timing,
|
|
381
|
-
}) => {
|
|
382
|
-
if (corsEnabled) {
|
|
383
|
-
const accessControlHeaders = generateAccessControlHeaders({
|
|
384
|
-
request,
|
|
385
|
-
accessControlAllowedOrigins,
|
|
386
|
-
accessControlAllowRequestOrigin,
|
|
387
|
-
accessControlAllowedMethods,
|
|
388
|
-
accessControlAllowRequestMethod,
|
|
389
|
-
accessControlAllowedHeaders,
|
|
390
|
-
accessControlAllowRequestHeaders,
|
|
391
|
-
accessControlAllowCredentials,
|
|
392
|
-
accessControlMaxAge,
|
|
393
|
-
sendServerTiming,
|
|
394
|
-
})
|
|
395
|
-
|
|
396
|
-
return {
|
|
397
|
-
status,
|
|
398
|
-
statusText,
|
|
399
|
-
headers: composeResponseHeaders(headers, accessControlHeaders),
|
|
400
|
-
body,
|
|
401
|
-
bodyEncoding,
|
|
402
|
-
timing,
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
return {
|
|
407
|
-
status,
|
|
408
|
-
statusText,
|
|
409
|
-
headers,
|
|
410
|
-
body,
|
|
411
|
-
bodyEncoding,
|
|
412
|
-
timing,
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
let timeout
|
|
417
|
-
try {
|
|
418
|
-
if (corsEnabled && request.method === "OPTIONS") {
|
|
419
|
-
return {
|
|
420
|
-
response: responsePropertiesToResponse({
|
|
421
|
-
status: 200,
|
|
422
|
-
headers: {
|
|
423
|
-
"content-length": 0,
|
|
424
|
-
},
|
|
425
|
-
}),
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
timeout = setTimeout(
|
|
430
|
-
() => requestWaitingCallback(request, { logger, requestWaitingMs }),
|
|
431
|
-
requestWaitingMs,
|
|
432
|
-
)
|
|
433
|
-
|
|
434
|
-
const responseProperties = await requestToResponse(request)
|
|
435
|
-
clearTimeout(timeout)
|
|
436
|
-
return {
|
|
437
|
-
response: responsePropertiesToResponse(responseProperties || {}),
|
|
438
|
-
}
|
|
439
|
-
} catch (error) {
|
|
440
|
-
clearTimeout(timeout)
|
|
441
|
-
return {
|
|
442
|
-
response: composeResponse(
|
|
443
|
-
responsePropertiesToResponse({
|
|
444
|
-
status: 500,
|
|
445
|
-
headers: {
|
|
446
|
-
// ensure error are not cached
|
|
447
|
-
"cache-control": "no-store",
|
|
448
|
-
"content-type": "text/plain",
|
|
449
|
-
},
|
|
450
|
-
}),
|
|
451
|
-
await serverInternalErrorToResponse(error, {
|
|
452
|
-
request,
|
|
453
|
-
sendServerInternalErrorDetails,
|
|
454
|
-
}),
|
|
455
|
-
),
|
|
456
|
-
error,
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
|
|
461
457
|
return {
|
|
462
458
|
getStatus: () => status,
|
|
463
459
|
origin: serverOrigin,
|
|
@@ -497,73 +493,22 @@ const createNodeServer = async ({
|
|
|
497
493
|
})
|
|
498
494
|
}
|
|
499
495
|
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
request: { headers },
|
|
506
|
-
accessControlAllowedOrigins,
|
|
507
|
-
accessControlAllowRequestOrigin,
|
|
508
|
-
accessControlAllowedMethods,
|
|
509
|
-
accessControlAllowRequestMethod,
|
|
510
|
-
accessControlAllowedHeaders,
|
|
511
|
-
accessControlAllowRequestHeaders,
|
|
512
|
-
accessControlAllowCredentials,
|
|
513
|
-
// by default OPTIONS request can be cache for a long time, it's not going to change soon ?
|
|
514
|
-
// we could put a lot here, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age
|
|
515
|
-
accessControlMaxAge = 600,
|
|
516
|
-
sendServerTiming,
|
|
517
|
-
} = {}) => {
|
|
518
|
-
const vary = []
|
|
519
|
-
|
|
520
|
-
const allowedOriginArray = [...accessControlAllowedOrigins]
|
|
521
|
-
if (accessControlAllowRequestOrigin) {
|
|
522
|
-
if ("origin" in headers && headers.origin !== "null") {
|
|
523
|
-
allowedOriginArray.push(headers.origin)
|
|
524
|
-
vary.push("origin")
|
|
525
|
-
} else if ("referer" in headers) {
|
|
526
|
-
allowedOriginArray.push(urlToOrigin(headers.referer))
|
|
527
|
-
vary.push("referer")
|
|
528
|
-
} else {
|
|
529
|
-
allowedOriginArray.push("*")
|
|
496
|
+
const generateResponseDescription = async ({ requestToResponse, request }) => {
|
|
497
|
+
try {
|
|
498
|
+
const returnValue = await requestToResponse(request)
|
|
499
|
+
return {
|
|
500
|
+
responseProperties: returnValue,
|
|
530
501
|
}
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
if (accessControlAllowRequestMethod && "access-control-request-method" in headers) {
|
|
535
|
-
const requestMethodName = headers["access-control-request-method"]
|
|
536
|
-
if (!allowedMethodArray.includes(requestMethodName)) {
|
|
537
|
-
allowedMethodArray.push(requestMethodName)
|
|
538
|
-
vary.push("access-control-request-method")
|
|
502
|
+
} catch (error) {
|
|
503
|
+
return {
|
|
504
|
+
error,
|
|
539
505
|
}
|
|
540
506
|
}
|
|
541
|
-
|
|
542
|
-
const allowedHeaderArray = [...accessControlAllowedHeaders]
|
|
543
|
-
if (accessControlAllowRequestHeaders && "access-control-request-headers" in headers) {
|
|
544
|
-
const requestHeaderNameArray = headers["access-control-request-headers"].split(", ")
|
|
545
|
-
requestHeaderNameArray.forEach((headerName) => {
|
|
546
|
-
const headerNameLowerCase = headerName.toLowerCase()
|
|
547
|
-
if (!allowedHeaderArray.includes(headerNameLowerCase)) {
|
|
548
|
-
allowedHeaderArray.push(headerNameLowerCase)
|
|
549
|
-
if (!vary.includes("access-control-request-headers")) {
|
|
550
|
-
vary.push("access-control-request-headers")
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
})
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
return {
|
|
557
|
-
"access-control-allow-origin": allowedOriginArray.join(", "),
|
|
558
|
-
"access-control-allow-methods": allowedMethodArray.join(", "),
|
|
559
|
-
"access-control-allow-headers": allowedHeaderArray.join(", "),
|
|
560
|
-
...(accessControlAllowCredentials ? { "access-control-allow-credentials": true } : {}),
|
|
561
|
-
"access-control-max-age": accessControlMaxAge,
|
|
562
|
-
...(sendServerTiming ? { "timing-allow-origin": allowedOriginArray.join(", ") } : {}),
|
|
563
|
-
...(vary.length ? { vary: vary.join(", ") } : {}),
|
|
564
|
-
}
|
|
565
507
|
}
|
|
566
508
|
|
|
509
|
+
const statusToStatusText = (status) =>
|
|
510
|
+
http.STATUS_CODES[status] || "not specified"
|
|
511
|
+
|
|
567
512
|
const composePredicate = (previousPredicate, predicate) => {
|
|
568
513
|
return (value) => {
|
|
569
514
|
return previousPredicate(value) || predicate(value)
|
package/src/stopReasons.js
CHANGED
|
@@ -8,6 +8,8 @@ export const STOP_REASON_INTERNAL_ERROR = createReason("Internal error")
|
|
|
8
8
|
export const STOP_REASON_PROCESS_SIGHUP = createReason("process SIGHUP")
|
|
9
9
|
export const STOP_REASON_PROCESS_SIGTERM = createReason("process SIGTERM")
|
|
10
10
|
export const STOP_REASON_PROCESS_SIGINT = createReason("process SIGINT")
|
|
11
|
-
export const STOP_REASON_PROCESS_BEFORE_EXIT = createReason(
|
|
11
|
+
export const STOP_REASON_PROCESS_BEFORE_EXIT = createReason(
|
|
12
|
+
"process before exit",
|
|
13
|
+
)
|
|
12
14
|
export const STOP_REASON_PROCESS_EXIT = createReason("process exit")
|
|
13
15
|
export const STOP_REASON_NOT_SPECIFIED = createReason("not specified")
|
package/src/urlToContentType.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// https://github.com/jshttp/mime-db/blob/master/src/apache-types.json
|
|
2
2
|
|
|
3
|
-
import { extname } from "path"
|
|
3
|
+
import { extname } from "node:path"
|
|
4
|
+
|
|
4
5
|
import { jsenvContentTypeMap } from "./jsenvContentTypeMap.js"
|
|
5
6
|
|
|
6
7
|
export const urlToContentType = (
|
|
@@ -9,7 +10,9 @@ export const urlToContentType = (
|
|
|
9
10
|
contentTypeDefault = "application/octet-stream",
|
|
10
11
|
) => {
|
|
11
12
|
if (typeof contentTypeMap !== "object") {
|
|
12
|
-
throw new TypeError(
|
|
13
|
+
throw new TypeError(
|
|
14
|
+
`contentTypeMap must be an object, got ${contentTypeMap}`,
|
|
15
|
+
)
|
|
13
16
|
}
|
|
14
17
|
const pathname = new URL(url).pathname
|
|
15
18
|
|
|
@@ -21,9 +24,13 @@ export const urlToContentType = (
|
|
|
21
24
|
|
|
22
25
|
const extension = extensionWithDot.slice(1)
|
|
23
26
|
const availableContentTypes = Object.keys(contentTypeMap)
|
|
24
|
-
const contentTypeForExtension = availableContentTypes.find(
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
const contentTypeForExtension = availableContentTypes.find(
|
|
28
|
+
(contentTypeName) => {
|
|
29
|
+
const contentType = contentTypeMap[contentTypeName]
|
|
30
|
+
return (
|
|
31
|
+
contentType.extensions && contentType.extensions.indexOf(extension) > -1
|
|
32
|
+
)
|
|
33
|
+
},
|
|
34
|
+
)
|
|
28
35
|
return contentTypeForExtension || contentTypeDefault
|
|
29
36
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export const jsenvAccessControlAllowedHeaders = ["x-requested-with"]
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export const jsenvAccessControlAllowedMethods = ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { firstOperationMatching } from "@jsenv/cancellation"
|
|
2
|
-
import { timeFunction } from "./serverTiming.js"
|
|
3
|
-
import { composeResponse } from "./composeResponse.js"
|
|
4
|
-
|
|
5
|
-
export const composeServices = (...callbacks) => {
|
|
6
|
-
return (request) => {
|
|
7
|
-
return firstOperationMatching({
|
|
8
|
-
array: callbacks,
|
|
9
|
-
start: (callback) => callback(request),
|
|
10
|
-
predicate: serviceGeneratedResponsePredicate,
|
|
11
|
-
})
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export const composeServicesWithTiming = (namedServices) => {
|
|
16
|
-
return async (request) => {
|
|
17
|
-
const servicesTiming = {}
|
|
18
|
-
const response = await firstOperationMatching({
|
|
19
|
-
array: Object.keys(namedServices).map((serviceName) => {
|
|
20
|
-
return { serviceName, serviceFn: namedServices[serviceName] }
|
|
21
|
-
}),
|
|
22
|
-
start: async ({ serviceName, serviceFn }) => {
|
|
23
|
-
const [serviceTiming, value] = await timeFunction(serviceName, () => serviceFn(request))
|
|
24
|
-
Object.assign(servicesTiming, serviceTiming)
|
|
25
|
-
return value
|
|
26
|
-
},
|
|
27
|
-
predicate: serviceGeneratedResponsePredicate,
|
|
28
|
-
})
|
|
29
|
-
if (response) {
|
|
30
|
-
return composeResponse({ timing: servicesTiming }, response)
|
|
31
|
-
}
|
|
32
|
-
return null
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const serviceGeneratedResponsePredicate = (value) => {
|
|
37
|
-
if (value === null) {
|
|
38
|
-
return false
|
|
39
|
-
}
|
|
40
|
-
return typeof value === "object"
|
|
41
|
-
}
|