@jsenv/server 7.3.1 → 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/main.js +28 -17
- package/package.json +4 -3
- package/src/{internal → content_negotiation}/applyContentNegotiation.js +0 -0
- package/src/{internal → content_negotiation}/checkContentNegotiation.js +3 -3
- package/src/{negotiateContentEncoding.js → content_negotiation/negotiateContentEncoding.js} +2 -2
- package/src/{negotiateContentLanguage.js → content_negotiation/negotiateContentLanguage.js} +2 -2
- package/src/{negotiateContentType.js → content_negotiation/negotiateContentType.js} +2 -2
- package/src/content_negotiation/plugin_content_negotiation_check.js +15 -0
- package/src/cors/plugin_cors.js +156 -0
- package/src/findFreePort.js +2 -1
- package/src/{composeResponse.js → internal/composeResponse.js} +2 -2
- package/src/{convertFileSystemErrorToResponseProperties.js → internal/convertFileSystemErrorToResponseProperties.js} +0 -0
- package/src/{headersToObject.js → internal/headersToObject.js} +0 -0
- package/src/internal/nodeRequestToRequest.js +1 -1
- package/src/jsenvServerInternalErrorToResponse.js +1 -1
- package/src/plugin_request_waiting_check.js +34 -0
- package/src/serveFile.js +40 -26
- package/src/server_timing/plugin_server_timing.js +28 -0
- package/src/{serverTiming.js → server_timing/timing_header.js} +30 -56
- package/src/server_timing/timing_measure.js +26 -0
- package/src/{service-composition.js → service_composition/service_composition.js} +3 -2
- package/src/{createSSERoom.js → sse/createSSERoom.js} +2 -1
- package/src/startServer.js +166 -272
- package/src/urlToContentType.js +2 -1
- package/src/jsenvAccessControlAllowedHeaders.js +0 -1
- package/src/jsenvAccessControlAllowedMethods.js +0 -7
- package/src/urlToSearchParamValue.js +0 -3
package/main.js
CHANGED
|
@@ -3,31 +3,16 @@
|
|
|
3
3
|
* Its responsability is to export what is documented
|
|
4
4
|
* Ideally this file should be kept simple to help discovering codebase progressively.
|
|
5
5
|
*/
|
|
6
|
-
/* eslint-disable import/max-dependencies */
|
|
7
6
|
|
|
8
|
-
export {
|
|
9
|
-
export { convertFileSystemErrorToResponseProperties } from "./src/convertFileSystemErrorToResponseProperties.js"
|
|
10
|
-
export { createSSERoom } from "./src/createSSERoom.js"
|
|
11
|
-
export { fetchUrl } from "./src/fetchUrl.js"
|
|
12
|
-
export { findFreePort } from "./src/findFreePort.js"
|
|
13
|
-
export { headersToObject } from "./src/headersToObject.js"
|
|
7
|
+
export { startServer } from "./src/startServer.js"
|
|
14
8
|
export {
|
|
15
9
|
composeServices,
|
|
16
10
|
composeServicesWithTiming,
|
|
17
11
|
nextService,
|
|
18
|
-
} from "./src/
|
|
19
|
-
export { jsenvAccessControlAllowedHeaders } from "./src/jsenvAccessControlAllowedHeaders.js"
|
|
20
|
-
export { jsenvAccessControlAllowedMethods } from "./src/jsenvAccessControlAllowedMethods.js"
|
|
12
|
+
} from "./src/service_composition/service_composition.js"
|
|
21
13
|
export { jsenvServerInternalErrorToResponse } from "./src/jsenvServerInternalErrorToResponse.js"
|
|
22
|
-
export { negotiateContentEncoding } from "./src/negotiateContentEncoding.js"
|
|
23
|
-
export { negotiateContentLanguage } from "./src/negotiateContentLanguage.js"
|
|
24
|
-
export { negotiateContentType } from "./src/negotiateContentType.js"
|
|
25
|
-
export { urlToContentType } from "./src/urlToContentType.js"
|
|
26
|
-
export { urlToSearchParamValue } from "./src/urlToSearchParamValue.js"
|
|
27
14
|
export { readRequestBody } from "./src/readRequestBody.js"
|
|
28
15
|
export { serveFile } from "./src/serveFile.js"
|
|
29
|
-
export { timeFunction, timeStart } from "./src/serverTiming.js"
|
|
30
|
-
export { startServer } from "./src/startServer.js"
|
|
31
16
|
export {
|
|
32
17
|
STOP_REASON_INTERNAL_ERROR,
|
|
33
18
|
STOP_REASON_PROCESS_SIGHUP,
|
|
@@ -37,3 +22,29 @@ export {
|
|
|
37
22
|
STOP_REASON_PROCESS_EXIT,
|
|
38
23
|
STOP_REASON_NOT_SPECIFIED,
|
|
39
24
|
} from "./src/stopReasons.js"
|
|
25
|
+
|
|
26
|
+
// CORS
|
|
27
|
+
export {
|
|
28
|
+
pluginCORS,
|
|
29
|
+
jsenvAccessControlAllowedHeaders,
|
|
30
|
+
jsenvAccessControlAllowedMethods,
|
|
31
|
+
} from "./src/cors/plugin_cors.js"
|
|
32
|
+
|
|
33
|
+
// server timings
|
|
34
|
+
export { pluginServerTiming } from "./src/server_timing/plugin_server_timing.js"
|
|
35
|
+
export { timeFunction, timeStart } from "./src/server_timing/timing_measure.js"
|
|
36
|
+
|
|
37
|
+
// SSE
|
|
38
|
+
export { createSSERoom } from "./src/sse/createSSERoom.js"
|
|
39
|
+
|
|
40
|
+
// content-negotiation
|
|
41
|
+
export { negotiateContentType } from "./src/content_negotiation/negotiateContentType.js"
|
|
42
|
+
export { negotiateContentEncoding } from "./src/content_negotiation/negotiateContentEncoding.js"
|
|
43
|
+
export { negotiateContentLanguage } from "./src/content_negotiation/negotiateContentLanguage.js"
|
|
44
|
+
export { pluginContentNegotiationCheck } from "./src/content_negotiation/plugin_content_negotiation_check.js"
|
|
45
|
+
|
|
46
|
+
// others
|
|
47
|
+
export { pluginRequestWaitingCheck } from "./src/plugin_request_waiting_check.js"
|
|
48
|
+
export { findFreePort } from "./src/findFreePort.js"
|
|
49
|
+
export { fetchUrl } from "./src/fetchUrl.js"
|
|
50
|
+
export { urlToContentType } from "./src/urlToContentType.js"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsenv/server",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "8.0.0",
|
|
4
4
|
"description": "Write your Node.js server using pure functions",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"node": ">=14.17.0"
|
|
17
17
|
},
|
|
18
18
|
"publishConfig": {
|
|
19
|
-
"access": "public"
|
|
19
|
+
"access": "public",
|
|
20
|
+
"registry": "https://registry.npmjs.org"
|
|
20
21
|
},
|
|
21
22
|
"type": "module",
|
|
22
23
|
"exports": {
|
|
@@ -33,7 +34,7 @@
|
|
|
33
34
|
"eslint-check": "node ./node_modules/eslint/bin/eslint.js . --ext=.js,.mjs",
|
|
34
35
|
"generate-importmap": "node ./script/importmap/generate_importmap.mjs",
|
|
35
36
|
"measure-performances": "node --expose-gc ./script/performance/generate_performance_report.mjs --log",
|
|
36
|
-
"test": "node ./script/test/test.mjs",
|
|
37
|
+
"test": "node --unhandled-rejections=strict ./script/test/test.mjs",
|
|
37
38
|
"test-with-coverage": "npm run test -- --coverage",
|
|
38
39
|
"generate-html-snapshots": "node ./test/internal_error/generate_html_snapshot_files.mjs",
|
|
39
40
|
"prettier-format": "node ./script/prettier/prettier_format.mjs",
|
|
File without changes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { negotiateContentType } from "
|
|
2
|
-
import { negotiateContentLanguage } from "
|
|
3
|
-
import { negotiateContentEncoding } from "
|
|
1
|
+
import { negotiateContentType } from "./negotiateContentType.js"
|
|
2
|
+
import { negotiateContentLanguage } from "./negotiateContentLanguage.js"
|
|
3
|
+
import { negotiateContentEncoding } from "./negotiateContentEncoding.js"
|
|
4
4
|
|
|
5
5
|
export const checkContentNegotiation = (request, response, { warn }) => {
|
|
6
6
|
const requestAcceptHeader = request.headers.accept
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { parseMultipleHeader } from "../internal/multiple-header.js"
|
|
2
|
+
import { applyContentNegotiation } from "./applyContentNegotiation.js"
|
|
3
3
|
|
|
4
4
|
export const negotiateContentEncoding = (request, availableEncodings) => {
|
|
5
5
|
const { headers = {} } = request
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { parseMultipleHeader } from "../internal/multiple-header.js"
|
|
2
|
+
import { applyContentNegotiation } from "./applyContentNegotiation.js"
|
|
3
3
|
|
|
4
4
|
export const negotiateContentLanguage = (request, availableLanguages) => {
|
|
5
5
|
const { headers = {} } = request
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { parseMultipleHeader } from "../internal/multiple-header.js"
|
|
2
|
+
import { applyContentNegotiation } from "./applyContentNegotiation.js"
|
|
3
3
|
|
|
4
4
|
export const negotiateContentType = (request, availableContentTypes) => {
|
|
5
5
|
const { headers = {} } = request
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { checkContentNegotiation } from "./checkContentNegotiation.js"
|
|
2
|
+
|
|
3
|
+
export const pluginContentNegotiationCheck = () => {
|
|
4
|
+
return {
|
|
5
|
+
content_negotiation_check: {
|
|
6
|
+
onRequest: (request, { warn }) => {
|
|
7
|
+
return {
|
|
8
|
+
onResponse: (response) => {
|
|
9
|
+
checkContentNegotiation(request, response, { warn })
|
|
10
|
+
},
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { urlToOrigin } from "@jsenv/filesystem"
|
|
2
|
+
|
|
3
|
+
export const jsenvAccessControlAllowedHeaders = ["x-requested-with"]
|
|
4
|
+
|
|
5
|
+
export const jsenvAccessControlAllowedMethods = [
|
|
6
|
+
"GET",
|
|
7
|
+
"POST",
|
|
8
|
+
"PUT",
|
|
9
|
+
"DELETE",
|
|
10
|
+
"OPTIONS",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
export const pluginCORS = ({
|
|
14
|
+
accessControlAllowedOrigins = [],
|
|
15
|
+
accessControlAllowedMethods = jsenvAccessControlAllowedMethods,
|
|
16
|
+
accessControlAllowedHeaders = jsenvAccessControlAllowedHeaders,
|
|
17
|
+
accessControlAllowRequestOrigin = false,
|
|
18
|
+
accessControlAllowRequestMethod = false,
|
|
19
|
+
accessControlAllowRequestHeaders = false,
|
|
20
|
+
accessControlAllowCredentials = false,
|
|
21
|
+
// by default OPTIONS request can be cache for a long time, it's not going to change soon ?
|
|
22
|
+
// we could put a lot here, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age
|
|
23
|
+
accessControlMaxAge = 600,
|
|
24
|
+
timingAllowOrigin,
|
|
25
|
+
} = {}) => {
|
|
26
|
+
// TODO: we should check access control params to throw or warn if we find strange values
|
|
27
|
+
|
|
28
|
+
const corsEnabled =
|
|
29
|
+
accessControlAllowRequestOrigin || accessControlAllowedOrigins.length
|
|
30
|
+
|
|
31
|
+
if (!corsEnabled) {
|
|
32
|
+
return {
|
|
33
|
+
cors: {},
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
cors: {
|
|
39
|
+
onServerParams: ({ plugins }) => {
|
|
40
|
+
if (timingAllowOrigin === undefined && plugins.server_timings) {
|
|
41
|
+
timingAllowOrigin = true
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
onRequest: (request, { shortcircuitResponse }) => {
|
|
46
|
+
if (request.method === "OPTIONS") {
|
|
47
|
+
// when request method is "OPTIONS" we must return a 200 without body
|
|
48
|
+
// So we bypass "requestToResponse" in that scenario using shortcircuitResponse
|
|
49
|
+
shortcircuitResponse({
|
|
50
|
+
status: 200,
|
|
51
|
+
headers: {
|
|
52
|
+
"content-length": 0,
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
onResponse: () => {
|
|
59
|
+
const accessControlHeaders = generateAccessControlHeaders({
|
|
60
|
+
request,
|
|
61
|
+
accessControlAllowedOrigins,
|
|
62
|
+
accessControlAllowRequestOrigin,
|
|
63
|
+
accessControlAllowedMethods,
|
|
64
|
+
accessControlAllowRequestMethod,
|
|
65
|
+
accessControlAllowedHeaders,
|
|
66
|
+
accessControlAllowRequestHeaders,
|
|
67
|
+
accessControlAllowCredentials,
|
|
68
|
+
accessControlMaxAge,
|
|
69
|
+
timingAllowOrigin,
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
headers: accessControlHeaders,
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// https://www.w3.org/TR/cors/
|
|
83
|
+
// https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
|
|
84
|
+
const generateAccessControlHeaders = ({
|
|
85
|
+
request: { headers },
|
|
86
|
+
accessControlAllowedOrigins,
|
|
87
|
+
accessControlAllowRequestOrigin,
|
|
88
|
+
accessControlAllowedMethods,
|
|
89
|
+
accessControlAllowRequestMethod,
|
|
90
|
+
accessControlAllowedHeaders,
|
|
91
|
+
accessControlAllowRequestHeaders,
|
|
92
|
+
accessControlAllowCredentials,
|
|
93
|
+
// by default OPTIONS request can be cache for a long time, it's not going to change soon ?
|
|
94
|
+
// we could put a lot here, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age
|
|
95
|
+
accessControlMaxAge = 600,
|
|
96
|
+
timingAllowOrigin,
|
|
97
|
+
} = {}) => {
|
|
98
|
+
const vary = []
|
|
99
|
+
|
|
100
|
+
const allowedOriginArray = [...accessControlAllowedOrigins]
|
|
101
|
+
if (accessControlAllowRequestOrigin) {
|
|
102
|
+
if ("origin" in headers && headers.origin !== "null") {
|
|
103
|
+
allowedOriginArray.push(headers.origin)
|
|
104
|
+
vary.push("origin")
|
|
105
|
+
} else if ("referer" in headers) {
|
|
106
|
+
allowedOriginArray.push(urlToOrigin(headers.referer))
|
|
107
|
+
vary.push("referer")
|
|
108
|
+
} else {
|
|
109
|
+
allowedOriginArray.push("*")
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const allowedMethodArray = [...accessControlAllowedMethods]
|
|
114
|
+
if (
|
|
115
|
+
accessControlAllowRequestMethod &&
|
|
116
|
+
"access-control-request-method" in headers
|
|
117
|
+
) {
|
|
118
|
+
const requestMethodName = headers["access-control-request-method"]
|
|
119
|
+
if (!allowedMethodArray.includes(requestMethodName)) {
|
|
120
|
+
allowedMethodArray.push(requestMethodName)
|
|
121
|
+
vary.push("access-control-request-method")
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const allowedHeaderArray = [...accessControlAllowedHeaders]
|
|
126
|
+
if (
|
|
127
|
+
accessControlAllowRequestHeaders &&
|
|
128
|
+
"access-control-request-headers" in headers
|
|
129
|
+
) {
|
|
130
|
+
const requestHeaderNameArray =
|
|
131
|
+
headers["access-control-request-headers"].split(", ")
|
|
132
|
+
requestHeaderNameArray.forEach((headerName) => {
|
|
133
|
+
const headerNameLowerCase = headerName.toLowerCase()
|
|
134
|
+
if (!allowedHeaderArray.includes(headerNameLowerCase)) {
|
|
135
|
+
allowedHeaderArray.push(headerNameLowerCase)
|
|
136
|
+
if (!vary.includes("access-control-request-headers")) {
|
|
137
|
+
vary.push("access-control-request-headers")
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
"access-control-allow-origin": allowedOriginArray.join(", "),
|
|
145
|
+
"access-control-allow-methods": allowedMethodArray.join(", "),
|
|
146
|
+
"access-control-allow-headers": allowedHeaderArray.join(", "),
|
|
147
|
+
...(accessControlAllowCredentials
|
|
148
|
+
? { "access-control-allow-credentials": true }
|
|
149
|
+
: {}),
|
|
150
|
+
"access-control-max-age": accessControlMaxAge,
|
|
151
|
+
...(timingAllowOrigin
|
|
152
|
+
? { "timing-allow-origin": allowedOriginArray.join(", ") }
|
|
153
|
+
: {}),
|
|
154
|
+
...(vary.length ? { vary: vary.join(", ") } : {}),
|
|
155
|
+
}
|
|
156
|
+
}
|
package/src/findFreePort.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { compositionMappingToComposeStrict } from "./
|
|
2
|
-
import { composeResponseHeaders } from "./
|
|
1
|
+
import { compositionMappingToComposeStrict } from "./compositionMappingToComposeStrict.js"
|
|
2
|
+
import { composeResponseHeaders } from "./composeResponseHeaders.js"
|
|
3
3
|
|
|
4
4
|
const responseCompositionMapping = {
|
|
5
5
|
status: (prevStatus, status) => status,
|
|
File without changes
|
|
File without changes
|
|
@@ -47,7 +47,7 @@ export const nodeRequestToRequest = (
|
|
|
47
47
|
const nodeRequestToCancellationToken = (nodeRequest) => {
|
|
48
48
|
const { cancel, token } = createCancellationSource()
|
|
49
49
|
nodeRequest.on("abort", () => {
|
|
50
|
-
cancel("request aborted")
|
|
50
|
+
cancel("request aborted by client")
|
|
51
51
|
})
|
|
52
52
|
nodeRequest.on("close", () => {
|
|
53
53
|
cancel("request closed")
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createDetailedMessage } from "@jsenv/logger"
|
|
2
|
+
|
|
3
|
+
export const pluginRequestWaitingCheck = ({
|
|
4
|
+
requestWaitingMs = 20000,
|
|
5
|
+
requestWaitingCallback = ({ request, logger, requestWaitingMs }) => {
|
|
6
|
+
logger.warn(
|
|
7
|
+
createDetailedMessage(
|
|
8
|
+
`still no response found for request after ${requestWaitingMs} ms`,
|
|
9
|
+
{
|
|
10
|
+
"request url": `${request.origin}${request.ressource}`,
|
|
11
|
+
"request headers": JSON.stringify(request.headers, null, " "),
|
|
12
|
+
},
|
|
13
|
+
),
|
|
14
|
+
)
|
|
15
|
+
},
|
|
16
|
+
} = {}) => {
|
|
17
|
+
return {
|
|
18
|
+
plugin_request_waiting_check: {
|
|
19
|
+
init: () => {},
|
|
20
|
+
|
|
21
|
+
onRequest: (request, { logger }) => {
|
|
22
|
+
const timeout = setTimeout(
|
|
23
|
+
() => requestWaitingCallback({ request, logger, requestWaitingMs }),
|
|
24
|
+
requestWaitingMs,
|
|
25
|
+
)
|
|
26
|
+
return {
|
|
27
|
+
onResponse: () => {
|
|
28
|
+
clearTimeout(timeout)
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/serveFile.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/* eslint-disable import/max-dependencies */
|
|
2
1
|
import { createReadStream, statSync } from "node:fs"
|
|
3
2
|
import {
|
|
4
3
|
assertAndNormalizeDirectoryUrl,
|
|
@@ -12,16 +11,15 @@ import {
|
|
|
12
11
|
} from "@jsenv/filesystem"
|
|
13
12
|
import { createOperation } from "@jsenv/cancellation"
|
|
14
13
|
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import { convertFileSystemErrorToResponseProperties } from "./convertFileSystemErrorToResponseProperties.js"
|
|
18
|
-
import { urlToContentType } from "./urlToContentType.js"
|
|
14
|
+
import { composeResponse } from "./internal/composeResponse.js"
|
|
15
|
+
import { convertFileSystemErrorToResponseProperties } from "./internal/convertFileSystemErrorToResponseProperties.js"
|
|
19
16
|
import { jsenvContentTypeMap } from "./jsenvContentTypeMap.js"
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
17
|
+
import { timeFunction } from "./server_timing/timing_measure.js"
|
|
18
|
+
import { negotiateContentType } from "./content_negotiation/negotiateContentType.js"
|
|
19
|
+
import { negotiateContentEncoding } from "./content_negotiation/negotiateContentEncoding.js"
|
|
20
|
+
import { urlToContentType } from "./urlToContentType.js"
|
|
22
21
|
|
|
23
|
-
const
|
|
24
|
-
const ETAG_CACHE_MAX_SIZE = 500
|
|
22
|
+
const ETAG_MEMORY_MAP = new Map()
|
|
25
23
|
|
|
26
24
|
export const serveFile = async (
|
|
27
25
|
request,
|
|
@@ -29,7 +27,8 @@ export const serveFile = async (
|
|
|
29
27
|
rootDirectoryUrl,
|
|
30
28
|
contentTypeMap = jsenvContentTypeMap,
|
|
31
29
|
etagEnabled = false,
|
|
32
|
-
|
|
30
|
+
etagMemory = true,
|
|
31
|
+
etagMemoryMaxSize = 1000,
|
|
33
32
|
mtimeEnabled = false,
|
|
34
33
|
compressionEnabled = false,
|
|
35
34
|
compressionSizeThreshold = 1024,
|
|
@@ -179,7 +178,8 @@ export const serveFile = async (
|
|
|
179
178
|
|
|
180
179
|
const clientCacheResponse = await getClientCacheResponse(request, {
|
|
181
180
|
etagEnabled,
|
|
182
|
-
|
|
181
|
+
etagMemory,
|
|
182
|
+
etagMemoryMaxSize,
|
|
183
183
|
mtimeEnabled,
|
|
184
184
|
sourceStat,
|
|
185
185
|
sourceUrl,
|
|
@@ -263,7 +263,14 @@ export const serveFile = async (
|
|
|
263
263
|
|
|
264
264
|
const getClientCacheResponse = async (
|
|
265
265
|
request,
|
|
266
|
-
{
|
|
266
|
+
{
|
|
267
|
+
etagEnabled,
|
|
268
|
+
etagMemory,
|
|
269
|
+
etagMemoryMaxSize,
|
|
270
|
+
mtimeEnabled,
|
|
271
|
+
sourceStat,
|
|
272
|
+
sourceUrl,
|
|
273
|
+
},
|
|
267
274
|
) => {
|
|
268
275
|
// here you might be tempted to add || headers["cache-control"] === "no-cache"
|
|
269
276
|
// but no-cache means ressource can be cache but must be revalidated (yeah naming is strange)
|
|
@@ -280,7 +287,8 @@ const getClientCacheResponse = async (
|
|
|
280
287
|
|
|
281
288
|
if (etagEnabled) {
|
|
282
289
|
return getEtagResponse(request, {
|
|
283
|
-
|
|
290
|
+
etagMemory,
|
|
291
|
+
etagMemoryMaxSize,
|
|
284
292
|
sourceStat,
|
|
285
293
|
sourceUrl,
|
|
286
294
|
})
|
|
@@ -297,11 +305,17 @@ const getClientCacheResponse = async (
|
|
|
297
305
|
|
|
298
306
|
const getEtagResponse = async (
|
|
299
307
|
request,
|
|
300
|
-
{
|
|
308
|
+
{ etagMemory, etagMemoryMaxSize, sourceUrl, sourceStat },
|
|
301
309
|
) => {
|
|
302
310
|
const [computeEtagTiming, fileContentEtag] = await timeFunction(
|
|
303
311
|
"file service>generate file etag",
|
|
304
|
-
() =>
|
|
312
|
+
() =>
|
|
313
|
+
computeEtag(request, {
|
|
314
|
+
etagMemory,
|
|
315
|
+
etagMemoryMaxSize,
|
|
316
|
+
sourceUrl,
|
|
317
|
+
sourceStat,
|
|
318
|
+
}),
|
|
305
319
|
)
|
|
306
320
|
|
|
307
321
|
const { headers = {} } = request
|
|
@@ -327,15 +341,15 @@ const getEtagResponse = async (
|
|
|
327
341
|
|
|
328
342
|
const computeEtag = async (
|
|
329
343
|
request,
|
|
330
|
-
{
|
|
344
|
+
{ etagMemory, etagMemoryMaxSize, sourceUrl, sourceStat },
|
|
331
345
|
) => {
|
|
332
|
-
if (
|
|
333
|
-
const
|
|
346
|
+
if (etagMemory) {
|
|
347
|
+
const etagMemoryEntry = ETAG_MEMORY_MAP.get(sourceUrl)
|
|
334
348
|
if (
|
|
335
|
-
|
|
336
|
-
fileStatAreTheSame(
|
|
349
|
+
etagMemoryEntry &&
|
|
350
|
+
fileStatAreTheSame(etagMemoryEntry.sourceStat, sourceStat)
|
|
337
351
|
) {
|
|
338
|
-
return
|
|
352
|
+
return etagMemoryEntry.eTag
|
|
339
353
|
}
|
|
340
354
|
}
|
|
341
355
|
const fileContentAsBuffer = await createOperation({
|
|
@@ -343,12 +357,12 @@ const computeEtag = async (
|
|
|
343
357
|
start: () => readFile(sourceUrl, { as: "buffer" }),
|
|
344
358
|
})
|
|
345
359
|
const eTag = bufferToEtag(fileContentAsBuffer)
|
|
346
|
-
if (
|
|
347
|
-
if (
|
|
348
|
-
const firstKey = Array.from(
|
|
349
|
-
|
|
360
|
+
if (etagMemory) {
|
|
361
|
+
if (ETAG_MEMORY_MAP.size >= etagMemoryMaxSize) {
|
|
362
|
+
const firstKey = Array.from(ETAG_MEMORY_MAP.keys())[0]
|
|
363
|
+
ETAG_MEMORY_MAP.delete(firstKey)
|
|
350
364
|
}
|
|
351
|
-
|
|
365
|
+
ETAG_MEMORY_MAP.set(sourceUrl, { sourceStat, eTag })
|
|
352
366
|
}
|
|
353
367
|
return eTag
|
|
354
368
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// https://www.w3.org/TR/server-timing/
|
|
2
|
+
|
|
3
|
+
import { performance } from "node:perf_hooks"
|
|
4
|
+
|
|
5
|
+
import { timingToServerTimingResponseHeaders } from "./timing_header.js"
|
|
6
|
+
|
|
7
|
+
export const pluginServerTiming = () => {
|
|
8
|
+
return {
|
|
9
|
+
server_timing: {
|
|
10
|
+
onRequest: () => {
|
|
11
|
+
const requestStartEvent = performance.now()
|
|
12
|
+
return {
|
|
13
|
+
onResponse: (response) => {
|
|
14
|
+
const responseEndEvent = performance.now()
|
|
15
|
+
const timeToCreateResponse = responseEndEvent - requestStartEvent
|
|
16
|
+
const serverTiming = {
|
|
17
|
+
...response.timing,
|
|
18
|
+
"time to start responding": timeToCreateResponse,
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
headers: timingToServerTimingResponseHeaders(serverTiming),
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -1,33 +1,7 @@
|
|
|
1
|
-
import { performance } from "perf_hooks"
|
|
2
1
|
import {
|
|
3
2
|
parseMultipleHeader,
|
|
4
3
|
stringifyMultipleHeader,
|
|
5
|
-
} from "
|
|
6
|
-
|
|
7
|
-
export const timeStart = (name) => {
|
|
8
|
-
// as specified in https://w3c.github.io/server-timing/#the-performanceservertiming-interface
|
|
9
|
-
// duration is a https://www.w3.org/TR/hr-time-2/#sec-domhighrestimestamp
|
|
10
|
-
const startTimestamp = performance.now()
|
|
11
|
-
const timeEnd = () => {
|
|
12
|
-
const endTimestamp = performance.now()
|
|
13
|
-
const timing = {
|
|
14
|
-
[name]: endTimestamp - startTimestamp,
|
|
15
|
-
}
|
|
16
|
-
return timing
|
|
17
|
-
}
|
|
18
|
-
return timeEnd
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export const timeFunction = (name, fn) => {
|
|
22
|
-
const timeEnd = timeStart(name)
|
|
23
|
-
const returnValue = fn()
|
|
24
|
-
if (returnValue && typeof returnValue.then === "function") {
|
|
25
|
-
return returnValue.then((value) => {
|
|
26
|
-
return [timeEnd(), value]
|
|
27
|
-
})
|
|
28
|
-
}
|
|
29
|
-
return [timeEnd(), returnValue]
|
|
30
|
-
}
|
|
4
|
+
} from "../internal/multiple-header.js"
|
|
31
5
|
|
|
32
6
|
// to predict order in chrome devtools we should put a,b,c,d,e or something
|
|
33
7
|
// because in chrome dev tools they are shown in alphabetic order
|
|
@@ -50,35 +24,6 @@ export const timingToServerTimingResponseHeaders = (timing) => {
|
|
|
50
24
|
return { "server-timing": serverTimingHeaderString }
|
|
51
25
|
}
|
|
52
26
|
|
|
53
|
-
const letters = [
|
|
54
|
-
"a",
|
|
55
|
-
"b",
|
|
56
|
-
"c",
|
|
57
|
-
"d",
|
|
58
|
-
"e",
|
|
59
|
-
"f",
|
|
60
|
-
"g",
|
|
61
|
-
"h",
|
|
62
|
-
"i",
|
|
63
|
-
"j",
|
|
64
|
-
"k",
|
|
65
|
-
"l",
|
|
66
|
-
"m",
|
|
67
|
-
"n",
|
|
68
|
-
"o",
|
|
69
|
-
"p",
|
|
70
|
-
"q",
|
|
71
|
-
"r",
|
|
72
|
-
"s",
|
|
73
|
-
"t",
|
|
74
|
-
"u",
|
|
75
|
-
"v",
|
|
76
|
-
"w",
|
|
77
|
-
"x",
|
|
78
|
-
"y",
|
|
79
|
-
"z",
|
|
80
|
-
]
|
|
81
|
-
|
|
82
27
|
export const parseServerTimingHeader = (serverTimingHeaderString) => {
|
|
83
28
|
const serverTimingHeaderObject = parseMultipleHeader(
|
|
84
29
|
serverTimingHeaderString,
|
|
@@ -121,3 +66,32 @@ const validateServerTimingName = (name) => {
|
|
|
121
66
|
}
|
|
122
67
|
return true
|
|
123
68
|
}
|
|
69
|
+
|
|
70
|
+
const letters = [
|
|
71
|
+
"a",
|
|
72
|
+
"b",
|
|
73
|
+
"c",
|
|
74
|
+
"d",
|
|
75
|
+
"e",
|
|
76
|
+
"f",
|
|
77
|
+
"g",
|
|
78
|
+
"h",
|
|
79
|
+
"i",
|
|
80
|
+
"j",
|
|
81
|
+
"k",
|
|
82
|
+
"l",
|
|
83
|
+
"m",
|
|
84
|
+
"n",
|
|
85
|
+
"o",
|
|
86
|
+
"p",
|
|
87
|
+
"q",
|
|
88
|
+
"r",
|
|
89
|
+
"s",
|
|
90
|
+
"t",
|
|
91
|
+
"u",
|
|
92
|
+
"v",
|
|
93
|
+
"w",
|
|
94
|
+
"x",
|
|
95
|
+
"y",
|
|
96
|
+
"z",
|
|
97
|
+
]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { performance } from "perf_hooks"
|
|
2
|
+
|
|
3
|
+
export const timeStart = (name) => {
|
|
4
|
+
// as specified in https://w3c.github.io/server-timing/#the-performanceservertiming-interface
|
|
5
|
+
// duration is a https://www.w3.org/TR/hr-time-2/#sec-domhighrestimestamp
|
|
6
|
+
const startTimestamp = performance.now()
|
|
7
|
+
const timeEnd = () => {
|
|
8
|
+
const endTimestamp = performance.now()
|
|
9
|
+
const timing = {
|
|
10
|
+
[name]: endTimestamp - startTimestamp,
|
|
11
|
+
}
|
|
12
|
+
return timing
|
|
13
|
+
}
|
|
14
|
+
return timeEnd
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const timeFunction = (name, fn) => {
|
|
18
|
+
const timeEnd = timeStart(name)
|
|
19
|
+
const returnValue = fn()
|
|
20
|
+
if (returnValue && typeof returnValue.then === "function") {
|
|
21
|
+
return returnValue.then((value) => {
|
|
22
|
+
return [timeEnd(), value]
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
return [timeEnd(), returnValue]
|
|
26
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { firstOperationMatching } from "@jsenv/cancellation"
|
|
2
|
-
|
|
3
|
-
import {
|
|
2
|
+
|
|
3
|
+
import { timeFunction } from "@jsenv/server/src/server_timing/timing_measure.js"
|
|
4
|
+
import { composeResponse } from "@jsenv/server/src/internal/composeResponse.js"
|
|
4
5
|
|
|
5
6
|
const nextSymbol = Symbol("next")
|
|
6
7
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { createLogger } from "@jsenv/logger"
|
|
2
|
+
|
|
2
3
|
import {
|
|
3
4
|
createObservable,
|
|
4
5
|
createCompositeProducer,
|
|
5
|
-
} from "
|
|
6
|
+
} from "@jsenv/server/src/internal/observable.js"
|
|
6
7
|
|
|
7
8
|
// https://www.html5rocks.com/en/tutorials/eventsource/basics/
|
|
8
9
|
export const createSSERoom = ({
|
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,
|
|
@@ -14,20 +12,22 @@ import {
|
|
|
14
12
|
unadvisedCrashSignal,
|
|
15
13
|
teardownSignal,
|
|
16
14
|
} from "@jsenv/node-signals"
|
|
17
|
-
import { memoize
|
|
18
|
-
import { createLogger } from "@jsenv/logger"
|
|
15
|
+
import { memoize } from "@jsenv/filesystem"
|
|
16
|
+
import { createDetailedMessage, createLogger } from "@jsenv/logger"
|
|
19
17
|
|
|
20
18
|
import { createTracker } from "./internal/createTracker.js"
|
|
21
19
|
import { createPolyglotServer } from "./internal/server-polyglot.js"
|
|
22
20
|
import { trackServerPendingConnections } from "./internal/trackServerPendingConnections.js"
|
|
23
21
|
import { trackServerPendingRequests } from "./internal/trackServerPendingRequests.js"
|
|
24
22
|
import { nodeRequestToRequest } from "./internal/nodeRequestToRequest.js"
|
|
25
|
-
import { composeResponseHeaders } from "./internal/composeResponseHeaders.js"
|
|
26
23
|
import { populateNodeResponse } from "./internal/populateNodeResponse.js"
|
|
27
24
|
import { colorizeResponseStatus } from "./internal/colorizeResponseStatus.js"
|
|
28
25
|
import { getServerOrigins } from "./internal/getServerOrigins.js"
|
|
29
26
|
import { listen, stopListening } from "./internal/listen.js"
|
|
30
|
-
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"
|
|
31
31
|
import {
|
|
32
32
|
STOP_REASON_INTERNAL_ERROR,
|
|
33
33
|
STOP_REASON_PROCESS_SIGHUP,
|
|
@@ -37,16 +37,6 @@ import {
|
|
|
37
37
|
STOP_REASON_PROCESS_EXIT,
|
|
38
38
|
STOP_REASON_NOT_SPECIFIED,
|
|
39
39
|
} from "./stopReasons.js"
|
|
40
|
-
import { jsenvAccessControlAllowedHeaders } from "./jsenvAccessControlAllowedHeaders.js"
|
|
41
|
-
import { jsenvAccessControlAllowedMethods } from "./jsenvAccessControlAllowedMethods.js"
|
|
42
|
-
import { listenRequest } from "./internal/listenRequest.js"
|
|
43
|
-
import {
|
|
44
|
-
timeFunction,
|
|
45
|
-
timingToServerTimingResponseHeaders,
|
|
46
|
-
} from "./serverTiming.js"
|
|
47
|
-
import { jsenvServerInternalErrorToResponse } from "./jsenvServerInternalErrorToResponse.js"
|
|
48
|
-
import { checkContentNegotiation } from "./internal/checkContentNegotiation.js"
|
|
49
|
-
import { listenServerConnectionError } from "./internal/listenServerConnectionError.js"
|
|
50
40
|
|
|
51
41
|
export const startServer = async ({
|
|
52
42
|
cancellationToken = createCancellationToken(),
|
|
@@ -73,36 +63,11 @@ export const startServer = async ({
|
|
|
73
63
|
stopOnCrash = false,
|
|
74
64
|
keepProcessAlive = true,
|
|
75
65
|
requestToResponse = () => null,
|
|
66
|
+
plugins = {},
|
|
76
67
|
|
|
77
|
-
accessControlAllowedOrigins = [],
|
|
78
|
-
accessControlAllowedMethods = jsenvAccessControlAllowedMethods,
|
|
79
|
-
accessControlAllowedHeaders = jsenvAccessControlAllowedHeaders,
|
|
80
|
-
accessControlAllowRequestOrigin = false,
|
|
81
|
-
accessControlAllowRequestMethod = false,
|
|
82
|
-
accessControlAllowRequestHeaders = false,
|
|
83
|
-
accessControlAllowCredentials = false,
|
|
84
|
-
// by default OPTIONS request can be cache for a long time, it's not going to change soon ?
|
|
85
|
-
// we could put a lot here, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age
|
|
86
|
-
accessControlMaxAge = 600,
|
|
87
|
-
|
|
88
|
-
// https://www.w3.org/TR/server-timing/
|
|
89
|
-
sendServerTiming = false,
|
|
90
68
|
sendServerInternalErrorDetails = false,
|
|
91
69
|
serverInternalErrorToResponse = jsenvServerInternalErrorToResponse,
|
|
92
70
|
|
|
93
|
-
requestWaitingMs = 20000,
|
|
94
|
-
requestWaitingCallback = (request, { logger }) => {
|
|
95
|
-
logger.warn(
|
|
96
|
-
`still no response found for request after ${requestWaitingMs} ms
|
|
97
|
-
--- request url ---
|
|
98
|
-
${request.origin}${request.ressource}
|
|
99
|
-
--- request headers ---
|
|
100
|
-
${JSON.stringify(request.headers, null, " ")}
|
|
101
|
-
`,
|
|
102
|
-
)
|
|
103
|
-
},
|
|
104
|
-
contentNegotiationWarnings = true,
|
|
105
|
-
|
|
106
71
|
startedCallback = () => {},
|
|
107
72
|
stoppedCallback = () => {},
|
|
108
73
|
errorIsCancellation = () => false,
|
|
@@ -296,85 +261,169 @@ ${JSON.stringify(request.headers, null, " ")}
|
|
|
296
261
|
serverCancellationToken,
|
|
297
262
|
serverOrigin,
|
|
298
263
|
})
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
264
|
+
|
|
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
|
|
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}`,
|
|
307
285
|
})
|
|
308
286
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
+
})
|
|
317
302
|
}
|
|
318
|
-
|
|
319
|
-
timingToServerTimingResponseHeaders(serverTiming),
|
|
320
|
-
response.headers,
|
|
321
|
-
)
|
|
303
|
+
})
|
|
322
304
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
305
|
+
const requestPlugins = []
|
|
306
|
+
let responseFromShortcircuit
|
|
307
|
+
const shortcircuitResponse = (value) => {
|
|
308
|
+
responseFromShortcircuit = value
|
|
326
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
|
+
})
|
|
327
325
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
|
338
346
|
}
|
|
339
347
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
+
}
|
|
348
391
|
}
|
|
349
392
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
+
})
|
|
355
406
|
|
|
356
407
|
if (
|
|
357
408
|
request.method !== "HEAD" &&
|
|
358
|
-
|
|
359
|
-
|
|
409
|
+
responseProperties.headers["content-length"] > 0 &&
|
|
410
|
+
responseProperties.body === ""
|
|
360
411
|
) {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
412
|
+
requestLogs.push({
|
|
413
|
+
type: "warn",
|
|
414
|
+
value: `content-length header is ${responseProperties.headers["content-length"]} but body is empty`,
|
|
415
|
+
})
|
|
364
416
|
}
|
|
365
417
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
logger.info(
|
|
374
|
-
`${colorizeResponseStatus(response.status)} ${response.statusText}`,
|
|
375
|
-
)
|
|
418
|
+
requestLogs.push({
|
|
419
|
+
type: "info",
|
|
420
|
+
value: `${colorizeResponseStatus(responseProperties.status)} ${
|
|
421
|
+
responseProperties.statusText
|
|
422
|
+
}`,
|
|
423
|
+
})
|
|
424
|
+
releaseRequestLogs()
|
|
376
425
|
|
|
377
|
-
populateNodeResponse(nodeResponse,
|
|
426
|
+
populateNodeResponse(nodeResponse, responseProperties, {
|
|
378
427
|
cancellationToken: request.cancellationToken,
|
|
379
428
|
ignoreBody: request.method === "HEAD",
|
|
380
429
|
// https://github.com/nodejs/node/blob/79296dc2d02c0b9872bbfcbb89148ea036a546d0/lib/internal/http2/compat.js#L97
|
|
@@ -384,12 +433,12 @@ ${request.method} ${request.origin}${request.ressource}`)
|
|
|
384
433
|
})
|
|
385
434
|
|
|
386
435
|
if (
|
|
387
|
-
|
|
436
|
+
error &&
|
|
388
437
|
// stopOnInternalError stops server only if requestToResponse generated
|
|
389
438
|
// a non controlled error (internal error).
|
|
390
439
|
// if requestToResponse gracefully produced a 500 response (it did not throw)
|
|
391
440
|
// then we can assume we are still in control of what we are doing
|
|
392
|
-
|
|
441
|
+
stopOnInternalError
|
|
393
442
|
) {
|
|
394
443
|
// il faudrais pouvoir stop que les autres response ?
|
|
395
444
|
stop(STOP_REASON_INTERNAL_ERROR)
|
|
@@ -405,98 +454,6 @@ ${request.method} ${request.origin}${request.ressource}`)
|
|
|
405
454
|
)
|
|
406
455
|
startedCallback({ origin: serverOrigin })
|
|
407
456
|
|
|
408
|
-
const corsEnabled =
|
|
409
|
-
accessControlAllowRequestOrigin || accessControlAllowedOrigins.length
|
|
410
|
-
// here we check access control options to throw or warn if we find strange values
|
|
411
|
-
|
|
412
|
-
const generateResponseDescription = async (request) => {
|
|
413
|
-
const responsePropertiesToResponse = ({
|
|
414
|
-
status = 501,
|
|
415
|
-
statusText = statusToStatusText(status),
|
|
416
|
-
headers = {},
|
|
417
|
-
body = "",
|
|
418
|
-
bodyEncoding,
|
|
419
|
-
timing,
|
|
420
|
-
}) => {
|
|
421
|
-
if (corsEnabled) {
|
|
422
|
-
const accessControlHeaders = generateAccessControlHeaders({
|
|
423
|
-
request,
|
|
424
|
-
accessControlAllowedOrigins,
|
|
425
|
-
accessControlAllowRequestOrigin,
|
|
426
|
-
accessControlAllowedMethods,
|
|
427
|
-
accessControlAllowRequestMethod,
|
|
428
|
-
accessControlAllowedHeaders,
|
|
429
|
-
accessControlAllowRequestHeaders,
|
|
430
|
-
accessControlAllowCredentials,
|
|
431
|
-
accessControlMaxAge,
|
|
432
|
-
sendServerTiming,
|
|
433
|
-
})
|
|
434
|
-
|
|
435
|
-
return {
|
|
436
|
-
status,
|
|
437
|
-
statusText,
|
|
438
|
-
headers: composeResponseHeaders(headers, accessControlHeaders),
|
|
439
|
-
body,
|
|
440
|
-
bodyEncoding,
|
|
441
|
-
timing,
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
return {
|
|
446
|
-
status,
|
|
447
|
-
statusText,
|
|
448
|
-
headers,
|
|
449
|
-
body,
|
|
450
|
-
bodyEncoding,
|
|
451
|
-
timing,
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
let timeout
|
|
456
|
-
try {
|
|
457
|
-
if (corsEnabled && request.method === "OPTIONS") {
|
|
458
|
-
return {
|
|
459
|
-
response: responsePropertiesToResponse({
|
|
460
|
-
status: 200,
|
|
461
|
-
headers: {
|
|
462
|
-
"content-length": 0,
|
|
463
|
-
},
|
|
464
|
-
}),
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
timeout = setTimeout(
|
|
469
|
-
() => requestWaitingCallback(request, { logger, requestWaitingMs }),
|
|
470
|
-
requestWaitingMs,
|
|
471
|
-
)
|
|
472
|
-
|
|
473
|
-
const responseProperties = await requestToResponse(request)
|
|
474
|
-
clearTimeout(timeout)
|
|
475
|
-
return {
|
|
476
|
-
response: responsePropertiesToResponse(responseProperties || {}),
|
|
477
|
-
}
|
|
478
|
-
} catch (error) {
|
|
479
|
-
clearTimeout(timeout)
|
|
480
|
-
return {
|
|
481
|
-
response: composeResponse(
|
|
482
|
-
responsePropertiesToResponse({
|
|
483
|
-
status: 500,
|
|
484
|
-
headers: {
|
|
485
|
-
// ensure error are not cached
|
|
486
|
-
"cache-control": "no-store",
|
|
487
|
-
"content-type": "text/plain",
|
|
488
|
-
},
|
|
489
|
-
}),
|
|
490
|
-
await serverInternalErrorToResponse(error, {
|
|
491
|
-
request,
|
|
492
|
-
sendServerInternalErrorDetails,
|
|
493
|
-
}),
|
|
494
|
-
),
|
|
495
|
-
error,
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
|
|
500
457
|
return {
|
|
501
458
|
getStatus: () => status,
|
|
502
459
|
origin: serverOrigin,
|
|
@@ -536,85 +493,22 @@ const createNodeServer = async ({
|
|
|
536
493
|
})
|
|
537
494
|
}
|
|
538
495
|
|
|
539
|
-
const
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
const generateAccessControlHeaders = ({
|
|
545
|
-
request: { headers },
|
|
546
|
-
accessControlAllowedOrigins,
|
|
547
|
-
accessControlAllowRequestOrigin,
|
|
548
|
-
accessControlAllowedMethods,
|
|
549
|
-
accessControlAllowRequestMethod,
|
|
550
|
-
accessControlAllowedHeaders,
|
|
551
|
-
accessControlAllowRequestHeaders,
|
|
552
|
-
accessControlAllowCredentials,
|
|
553
|
-
// by default OPTIONS request can be cache for a long time, it's not going to change soon ?
|
|
554
|
-
// we could put a lot here, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age
|
|
555
|
-
accessControlMaxAge = 600,
|
|
556
|
-
sendServerTiming,
|
|
557
|
-
} = {}) => {
|
|
558
|
-
const vary = []
|
|
559
|
-
|
|
560
|
-
const allowedOriginArray = [...accessControlAllowedOrigins]
|
|
561
|
-
if (accessControlAllowRequestOrigin) {
|
|
562
|
-
if ("origin" in headers && headers.origin !== "null") {
|
|
563
|
-
allowedOriginArray.push(headers.origin)
|
|
564
|
-
vary.push("origin")
|
|
565
|
-
} else if ("referer" in headers) {
|
|
566
|
-
allowedOriginArray.push(urlToOrigin(headers.referer))
|
|
567
|
-
vary.push("referer")
|
|
568
|
-
} else {
|
|
569
|
-
allowedOriginArray.push("*")
|
|
496
|
+
const generateResponseDescription = async ({ requestToResponse, request }) => {
|
|
497
|
+
try {
|
|
498
|
+
const returnValue = await requestToResponse(request)
|
|
499
|
+
return {
|
|
500
|
+
responseProperties: returnValue,
|
|
570
501
|
}
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
if (
|
|
575
|
-
accessControlAllowRequestMethod &&
|
|
576
|
-
"access-control-request-method" in headers
|
|
577
|
-
) {
|
|
578
|
-
const requestMethodName = headers["access-control-request-method"]
|
|
579
|
-
if (!allowedMethodArray.includes(requestMethodName)) {
|
|
580
|
-
allowedMethodArray.push(requestMethodName)
|
|
581
|
-
vary.push("access-control-request-method")
|
|
502
|
+
} catch (error) {
|
|
503
|
+
return {
|
|
504
|
+
error,
|
|
582
505
|
}
|
|
583
506
|
}
|
|
584
|
-
|
|
585
|
-
const allowedHeaderArray = [...accessControlAllowedHeaders]
|
|
586
|
-
if (
|
|
587
|
-
accessControlAllowRequestHeaders &&
|
|
588
|
-
"access-control-request-headers" in headers
|
|
589
|
-
) {
|
|
590
|
-
const requestHeaderNameArray =
|
|
591
|
-
headers["access-control-request-headers"].split(", ")
|
|
592
|
-
requestHeaderNameArray.forEach((headerName) => {
|
|
593
|
-
const headerNameLowerCase = headerName.toLowerCase()
|
|
594
|
-
if (!allowedHeaderArray.includes(headerNameLowerCase)) {
|
|
595
|
-
allowedHeaderArray.push(headerNameLowerCase)
|
|
596
|
-
if (!vary.includes("access-control-request-headers")) {
|
|
597
|
-
vary.push("access-control-request-headers")
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
})
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
return {
|
|
604
|
-
"access-control-allow-origin": allowedOriginArray.join(", "),
|
|
605
|
-
"access-control-allow-methods": allowedMethodArray.join(", "),
|
|
606
|
-
"access-control-allow-headers": allowedHeaderArray.join(", "),
|
|
607
|
-
...(accessControlAllowCredentials
|
|
608
|
-
? { "access-control-allow-credentials": true }
|
|
609
|
-
: {}),
|
|
610
|
-
"access-control-max-age": accessControlMaxAge,
|
|
611
|
-
...(sendServerTiming
|
|
612
|
-
? { "timing-allow-origin": allowedOriginArray.join(", ") }
|
|
613
|
-
: {}),
|
|
614
|
-
...(vary.length ? { vary: vary.join(", ") } : {}),
|
|
615
|
-
}
|
|
616
507
|
}
|
|
617
508
|
|
|
509
|
+
const statusToStatusText = (status) =>
|
|
510
|
+
http.STATUS_CODES[status] || "not specified"
|
|
511
|
+
|
|
618
512
|
const composePredicate = (previousPredicate, predicate) => {
|
|
619
513
|
return (value) => {
|
|
620
514
|
return previousPredicate(value) || predicate(value)
|
package/src/urlToContentType.js
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export const jsenvAccessControlAllowedHeaders = ["x-requested-with"]
|