@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.
Files changed (27) hide show
  1. package/main.js +28 -17
  2. package/package.json +4 -3
  3. package/src/{internal → content_negotiation}/applyContentNegotiation.js +0 -0
  4. package/src/{internal → content_negotiation}/checkContentNegotiation.js +3 -3
  5. package/src/{negotiateContentEncoding.js → content_negotiation/negotiateContentEncoding.js} +2 -2
  6. package/src/{negotiateContentLanguage.js → content_negotiation/negotiateContentLanguage.js} +2 -2
  7. package/src/{negotiateContentType.js → content_negotiation/negotiateContentType.js} +2 -2
  8. package/src/content_negotiation/plugin_content_negotiation_check.js +15 -0
  9. package/src/cors/plugin_cors.js +156 -0
  10. package/src/findFreePort.js +2 -1
  11. package/src/{composeResponse.js → internal/composeResponse.js} +2 -2
  12. package/src/{convertFileSystemErrorToResponseProperties.js → internal/convertFileSystemErrorToResponseProperties.js} +0 -0
  13. package/src/{headersToObject.js → internal/headersToObject.js} +0 -0
  14. package/src/internal/nodeRequestToRequest.js +1 -1
  15. package/src/jsenvServerInternalErrorToResponse.js +1 -1
  16. package/src/plugin_request_waiting_check.js +34 -0
  17. package/src/serveFile.js +40 -26
  18. package/src/server_timing/plugin_server_timing.js +28 -0
  19. package/src/{serverTiming.js → server_timing/timing_header.js} +30 -56
  20. package/src/server_timing/timing_measure.js +26 -0
  21. package/src/{service-composition.js → service_composition/service_composition.js} +3 -2
  22. package/src/{createSSERoom.js → sse/createSSERoom.js} +2 -1
  23. package/src/startServer.js +166 -272
  24. package/src/urlToContentType.js +2 -1
  25. package/src/jsenvAccessControlAllowedHeaders.js +0 -1
  26. package/src/jsenvAccessControlAllowedMethods.js +0 -7
  27. 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 { composeResponse } from "./src/composeResponse.js"
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/service-composition.js"
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": "7.3.1",
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",
@@ -1,6 +1,6 @@
1
- import { negotiateContentType } from "../negotiateContentType.js"
2
- import { negotiateContentLanguage } from "../negotiateContentLanguage.js"
3
- import { negotiateContentEncoding } from "../negotiateContentEncoding.js"
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 { applyContentNegotiation } from "./internal/applyContentNegotiation.js"
2
- import { parseMultipleHeader } from "./internal/multiple-header.js"
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 { applyContentNegotiation } from "./internal/applyContentNegotiation.js"
2
- import { parseMultipleHeader } from "./internal/multiple-header.js"
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 { applyContentNegotiation } from "./internal/applyContentNegotiation.js"
2
- import { parseMultipleHeader } from "./internal/multiple-header.js"
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
+ }
@@ -1,5 +1,6 @@
1
- import { createServer } from "net"
1
+ import { createServer } from "node:net"
2
2
  import { createCancellationToken } from "@jsenv/cancellation"
3
+
3
4
  import { listen } from "./internal/listen.js"
4
5
 
5
6
  export const findFreePort = async (
@@ -1,5 +1,5 @@
1
- import { compositionMappingToComposeStrict } from "./internal/compositionMappingToComposeStrict.js"
2
- import { composeResponseHeaders } from "./internal/composeResponseHeaders.js"
1
+ import { compositionMappingToComposeStrict } from "./compositionMappingToComposeStrict.js"
2
+ import { composeResponseHeaders } from "./composeResponseHeaders.js"
3
3
 
4
4
  const responseCompositionMapping = {
5
5
  status: (prevStatus, status) => status,
@@ -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")
@@ -1,4 +1,4 @@
1
- import { negotiateContentType } from "./negotiateContentType.js"
1
+ import { negotiateContentType } from "./content_negotiation/negotiateContentType.js"
2
2
 
3
3
  export const jsenvServerInternalErrorToResponse = (
4
4
  serverInternalError,
@@ -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 { negotiateContentType } from "./negotiateContentType.js"
16
- import { timeFunction } from "./serverTiming.js"
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 { composeResponse } from "./composeResponse.js"
21
- import { negotiateContentEncoding } from "./negotiateContentEncoding.js"
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 ETAG_CACHE = new Map()
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
- etagCacheDisabled = false,
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
- etagCacheDisabled,
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
- { etagEnabled, etagCacheDisabled, mtimeEnabled, sourceStat, sourceUrl },
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
- etagCacheDisabled,
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
- { etagCacheDisabled, sourceUrl, sourceStat },
308
+ { etagMemory, etagMemoryMaxSize, sourceUrl, sourceStat },
301
309
  ) => {
302
310
  const [computeEtagTiming, fileContentEtag] = await timeFunction(
303
311
  "file service>generate file etag",
304
- () => computeEtag(request, { etagCacheDisabled, sourceUrl, sourceStat }),
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
- { etagCacheDisabled, sourceUrl, sourceStat },
344
+ { etagMemory, etagMemoryMaxSize, sourceUrl, sourceStat },
331
345
  ) => {
332
- if (!etagCacheDisabled) {
333
- const etagCacheEntry = ETAG_CACHE.get(sourceUrl)
346
+ if (etagMemory) {
347
+ const etagMemoryEntry = ETAG_MEMORY_MAP.get(sourceUrl)
334
348
  if (
335
- etagCacheEntry &&
336
- fileStatAreTheSame(etagCacheEntry.sourceStat, sourceStat)
349
+ etagMemoryEntry &&
350
+ fileStatAreTheSame(etagMemoryEntry.sourceStat, sourceStat)
337
351
  ) {
338
- return etagCacheEntry.eTag
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 (!etagCacheDisabled) {
347
- if (ETAG_CACHE.size >= ETAG_CACHE_MAX_SIZE) {
348
- const firstKey = Array.from(ETAG_CACHE.keys())[0]
349
- ETAG_CACHE.delete(firstKey)
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
- ETAG_CACHE.set(sourceUrl, { sourceStat, eTag })
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 "./internal/multiple-header.js"
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
- import { timeFunction } from "./serverTiming.js"
3
- import { composeResponse } from "./composeResponse.js"
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 "./internal/observable.js"
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 = ({
@@ -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, urlToOrigin } from "@jsenv/filesystem"
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
- nodeRequest.on("error", (error) => {
300
- logger[
301
- error.message === "aborted" ? "debug" : "error"
302
- ](`error on request.
303
- --- request ressource ---
304
- ${request.ressource}
305
- --- error stack ---
306
- ${error.stack}`)
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
- const [startRespondingTiming, { response, error }] = await timeFunction(
310
- "time to start responding",
311
- () => generateResponseDescription(request),
312
- )
313
- if (sendServerTiming) {
314
- const serverTiming = {
315
- ...response.timing,
316
- ...startRespondingTiming,
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
- response.headers = composeResponseHeaders(
319
- timingToServerTimingResponseHeaders(serverTiming),
320
- response.headers,
321
- )
303
+ })
322
304
 
323
- if (contentNegotiationWarnings) {
324
- checkContentNegotiation(request, response, { warn: logger.warn })
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
- logger.info(`${request.method} ${request.origin}${request.ressource}`)
329
-
330
- if (
331
- error &&
332
- isCancelError(error) &&
333
- internalCancellationToken.cancellationRequested
334
- ) {
335
- logger.info("ignored because server closing")
336
- nodeResponse.destroy()
337
- return
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
- if (
341
- error &&
342
- isCancelError(error) &&
343
- request.cancellationToken.cancellationRequested
344
- ) {
345
- logger.info("ignored because request canceled")
346
- nodeResponse.destroy()
347
- return
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
- if (request.aborted) {
351
- logger.info(`request aborted by client`)
352
- nodeResponse.destroy()
353
- return
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
- response.headers["content-length"] > 0 &&
359
- response.body === ""
409
+ responseProperties.headers["content-length"] > 0 &&
410
+ responseProperties.body === ""
360
411
  ) {
361
- logger.warn(
362
- `content-length header is ${response.headers["content-length"]} but body is empty`,
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
- if (error) {
367
- logger.error(`internal error while handling request.
368
- --- error stack ---
369
- ${error.stack}
370
- --- request ---
371
- ${request.method} ${request.origin}${request.ressource}`)
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, response, {
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
- stopOnInternalError &&
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
- error
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 statusToStatusText = (status) =>
540
- http.STATUS_CODES[status] || "not specified"
541
-
542
- // https://www.w3.org/TR/cors/
543
- // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
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
- const allowedMethodArray = [...accessControlAllowedMethods]
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)
@@ -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 = (
@@ -1 +0,0 @@
1
- export const jsenvAccessControlAllowedHeaders = ["x-requested-with"]
@@ -1,7 +0,0 @@
1
- export const jsenvAccessControlAllowedMethods = [
2
- "GET",
3
- "POST",
4
- "PUT",
5
- "DELETE",
6
- "OPTIONS",
7
- ]
@@ -1,3 +0,0 @@
1
- export const urlToSearchParamValue = (url, searchParamName) => {
2
- return new URL(url).searchParams.get(searchParamName)
3
- }