@jsenv/server 12.4.2 → 12.5.3
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 +1 -1
- package/package.json +4 -5
- package/src/{jsenvServerInternalErrorToResponse.js → error_to_response_default.js} +9 -5
- package/src/fetch_filesystem.js +8 -78
- package/src/internal/convertFileSystemErrorToResponseProperties.js +1 -7
- package/src/serve_directory.js +76 -0
- package/src/startServer.js +12 -10
package/main.js
CHANGED
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
export { startServer } from "./src/startServer.js"
|
|
8
8
|
export { composeServices } from "./src/service_composition/service_composition.js"
|
|
9
9
|
export { setupRoutes } from "./src/service_composition/routing.js"
|
|
10
|
-
export { jsenvServerInternalErrorToResponse } from "./src/jsenvServerInternalErrorToResponse.js"
|
|
11
10
|
export { readRequestBody } from "./src/readRequestBody.js"
|
|
12
11
|
export { fetchFileSystem } from "./src/fetch_filesystem.js"
|
|
13
12
|
export {
|
|
@@ -42,6 +41,7 @@ export { pluginContentNegotiationCheck } from "./src/content_negotiation/plugin_
|
|
|
42
41
|
|
|
43
42
|
// others
|
|
44
43
|
export { fetchUrl } from "./src/fetch_url.js"
|
|
44
|
+
export { serveDirectory } from "./src/serve_directory.js"
|
|
45
45
|
export { fromFetchResponse } from "./src/from_fetch_response.js"
|
|
46
46
|
export { composeTwoResponses } from "./src/internal/response_composition.js"
|
|
47
47
|
export { pluginRessourceAliases } from "./src/ressource_aliases/plugin_ressource_aliases.js"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsenv/server",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.5.3",
|
|
4
4
|
"description": "Write your Node.js server using pure functions",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -16,8 +16,7 @@
|
|
|
16
16
|
"node": ">=16.13.0"
|
|
17
17
|
},
|
|
18
18
|
"publishConfig": {
|
|
19
|
-
"access": "public"
|
|
20
|
-
"registry": "https://registry.npmjs.org"
|
|
19
|
+
"access": "public"
|
|
21
20
|
},
|
|
22
21
|
"type": "module",
|
|
23
22
|
"exports": {
|
|
@@ -35,9 +34,9 @@
|
|
|
35
34
|
"importmap": "node ./script/importmap/importmap.mjs",
|
|
36
35
|
"eslint": "node ./node_modules/eslint/bin/eslint.js . --ext=.js,.mjs",
|
|
37
36
|
"performance": "node --expose-gc ./script/performance/performance.mjs --log",
|
|
37
|
+
"snapshot": "node ./test/internal_error/generate_html_snapshot_files.mjs",
|
|
38
38
|
"test": "node ./script/test/test.mjs",
|
|
39
39
|
"test-with-coverage": "npm run test -- --coverage",
|
|
40
|
-
"generate-html-snapshots": "node ./test/internal_error/generate_html_snapshot_files.mjs",
|
|
41
40
|
"prettier": "prettier --write .",
|
|
42
41
|
"certificate-install": "node ./script/certificate/certificate_install.mjs",
|
|
43
42
|
"playwright-install": "npx playwright install-deps && npx playwright install"
|
|
@@ -68,4 +67,4 @@
|
|
|
68
67
|
"playwright": "1.18.1",
|
|
69
68
|
"prettier": "2.5.1"
|
|
70
69
|
}
|
|
71
|
-
}
|
|
70
|
+
}
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { negotiateContentType } from "./content_negotiation/negotiateContentType.js"
|
|
2
2
|
|
|
3
|
-
export const
|
|
3
|
+
export const applyDefaultErrorToResponse = (
|
|
4
4
|
serverInternalError,
|
|
5
|
-
{ request,
|
|
5
|
+
{ request, sendErrorDetails = false },
|
|
6
6
|
) => {
|
|
7
7
|
const serverInternalErrorIsAPrimitive =
|
|
8
8
|
serverInternalError === null ||
|
|
9
9
|
(typeof serverInternalError !== "object" &&
|
|
10
10
|
typeof serverInternalError !== "function")
|
|
11
11
|
|
|
12
|
+
if (!serverInternalErrorIsAPrimitive && serverInternalError.asResponse) {
|
|
13
|
+
return serverInternalError.asResponse()
|
|
14
|
+
}
|
|
15
|
+
|
|
12
16
|
const dataToSend = serverInternalErrorIsAPrimitive
|
|
13
17
|
? {
|
|
14
18
|
code: "VALUE_THROWED",
|
|
@@ -16,7 +20,7 @@ export const jsenvServerInternalErrorToResponse = (
|
|
|
16
20
|
}
|
|
17
21
|
: {
|
|
18
22
|
code: serverInternalError.code || "UNKNOWN_ERROR",
|
|
19
|
-
...(
|
|
23
|
+
...(sendErrorDetails
|
|
20
24
|
? {
|
|
21
25
|
stack: serverInternalError.stack,
|
|
22
26
|
...serverInternalError,
|
|
@@ -27,7 +31,7 @@ export const jsenvServerInternalErrorToResponse = (
|
|
|
27
31
|
const availableContentTypes = {
|
|
28
32
|
"text/html": () => {
|
|
29
33
|
const renderHtmlForErrorWithoutDetails = () => {
|
|
30
|
-
return `<p>Details not available: to enable them server must be started with
|
|
34
|
+
return `<p>Details not available: to enable them server must be started with sendErrorDetails: true.</p>`
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
const renderHtmlForErrorWithDetails = () => {
|
|
@@ -55,7 +59,7 @@ export const jsenvServerInternalErrorToResponse = (
|
|
|
55
59
|
<details>
|
|
56
60
|
<summary>See internal error details</summary>
|
|
57
61
|
${
|
|
58
|
-
|
|
62
|
+
sendErrorDetails
|
|
59
63
|
? renderHtmlForErrorWithDetails()
|
|
60
64
|
: renderHtmlForErrorWithoutDetails()
|
|
61
65
|
}
|
package/src/fetch_filesystem.js
CHANGED
|
@@ -16,9 +16,9 @@ import { composeTwoResponses } from "./internal/response_composition.js"
|
|
|
16
16
|
import { convertFileSystemErrorToResponseProperties } from "./internal/convertFileSystemErrorToResponseProperties.js"
|
|
17
17
|
import { jsenvContentTypeMap } from "./jsenvContentTypeMap.js"
|
|
18
18
|
import { timeFunction } from "./server_timing/timing_measure.js"
|
|
19
|
-
import { negotiateContentType } from "./content_negotiation/negotiateContentType.js"
|
|
20
19
|
import { negotiateContentEncoding } from "./content_negotiation/negotiateContentEncoding.js"
|
|
21
20
|
import { urlToContentType } from "./urlToContentType.js"
|
|
21
|
+
import { serveDirectory } from "./serve_directory.js"
|
|
22
22
|
|
|
23
23
|
const ETAG_MEMORY_MAP = new Map()
|
|
24
24
|
|
|
@@ -39,7 +39,7 @@ export const fetchFileSystem = async (
|
|
|
39
39
|
? "private,max-age=0,must-revalidate"
|
|
40
40
|
: "no-store",
|
|
41
41
|
canReadDirectory = false,
|
|
42
|
-
rootDirectoryUrl
|
|
42
|
+
rootDirectoryUrl,
|
|
43
43
|
} = {},
|
|
44
44
|
) => {
|
|
45
45
|
const urlString = asUrlString(filesystemUrl)
|
|
@@ -88,83 +88,13 @@ export const fetchFileSystem = async (
|
|
|
88
88
|
"file service>read file stat",
|
|
89
89
|
() => statSync(sourceFilesystemPath),
|
|
90
90
|
)
|
|
91
|
-
|
|
92
91
|
if (sourceStat.isDirectory()) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
status: 403,
|
|
99
|
-
statusText: "not allowed to read directory",
|
|
100
|
-
timing: readStatTiming,
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const [readDirectoryTiming, directoryContentArray] = await timeFunction(
|
|
105
|
-
"file service>read directory",
|
|
106
|
-
() => promises.readdir(sourceFilesystemPath),
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
const responseProducers = {
|
|
110
|
-
"application/json": () => {
|
|
111
|
-
const directoryContentJson = JSON.stringify(directoryContentArray)
|
|
112
|
-
return {
|
|
113
|
-
status: 200,
|
|
114
|
-
headers: {
|
|
115
|
-
"content-type": "application/json",
|
|
116
|
-
"content-length": directoryContentJson.length,
|
|
117
|
-
},
|
|
118
|
-
body: directoryContentJson,
|
|
119
|
-
timing: {
|
|
120
|
-
...readStatTiming,
|
|
121
|
-
...readDirectoryTiming,
|
|
122
|
-
},
|
|
123
|
-
}
|
|
124
|
-
},
|
|
125
|
-
"text/html": () => {
|
|
126
|
-
const directoryAsHtml = `<!DOCTYPE html>
|
|
127
|
-
<html>
|
|
128
|
-
<head>
|
|
129
|
-
<title>Directory explorer</title>
|
|
130
|
-
<meta charset="utf-8" />
|
|
131
|
-
<link rel="icon" href="data:," />
|
|
132
|
-
</head>
|
|
133
|
-
|
|
134
|
-
<body>
|
|
135
|
-
<h1>Content of directory ${sourceUrl}</h1>
|
|
136
|
-
<ul>
|
|
137
|
-
${directoryContentArray.map((filename) => {
|
|
138
|
-
const fileUrl = String(new URL(filename, sourceUrl))
|
|
139
|
-
const fileUrlRelativeToServer = fileUrl.slice(
|
|
140
|
-
String(rootDirectoryUrl).length,
|
|
141
|
-
)
|
|
142
|
-
return `<li>
|
|
143
|
-
<a href="/${fileUrlRelativeToServer}">${fileUrlRelativeToServer}</a>
|
|
144
|
-
</li>`
|
|
145
|
-
}).join(`
|
|
146
|
-
`)}
|
|
147
|
-
</ul>
|
|
148
|
-
</body>
|
|
149
|
-
</html>`
|
|
150
|
-
|
|
151
|
-
return {
|
|
152
|
-
status: 200,
|
|
153
|
-
headers: {
|
|
154
|
-
"content-type": "text/html",
|
|
155
|
-
"content-length": Buffer.byteLength(directoryAsHtml),
|
|
156
|
-
},
|
|
157
|
-
body: directoryAsHtml,
|
|
158
|
-
}
|
|
159
|
-
},
|
|
160
|
-
}
|
|
161
|
-
const bestContentType = negotiateContentType(
|
|
162
|
-
{ headers },
|
|
163
|
-
Object.keys(responseProducers),
|
|
164
|
-
)
|
|
165
|
-
return responseProducers[bestContentType || "application/json"]()
|
|
92
|
+
return serveDirectory(urlString, {
|
|
93
|
+
headers,
|
|
94
|
+
canReadDirectory,
|
|
95
|
+
rootDirectoryUrl,
|
|
96
|
+
})
|
|
166
97
|
}
|
|
167
|
-
|
|
168
98
|
// not a file, give up
|
|
169
99
|
if (!sourceStat.isFile()) {
|
|
170
100
|
return {
|
|
@@ -239,7 +169,7 @@ export const fetchFileSystem = async (
|
|
|
239
169
|
...(cacheControl ? { "cache-control": cacheControl } : {}),
|
|
240
170
|
},
|
|
241
171
|
},
|
|
242
|
-
convertFileSystemErrorToResponseProperties(e),
|
|
172
|
+
convertFileSystemErrorToResponseProperties(e) || {},
|
|
243
173
|
)
|
|
244
174
|
}
|
|
245
175
|
}
|
|
@@ -6,21 +6,18 @@ export const convertFileSystemErrorToResponseProperties = (error) => {
|
|
|
6
6
|
statusText: `EACCES: No permission to read file at ${error.path}`,
|
|
7
7
|
}
|
|
8
8
|
}
|
|
9
|
-
|
|
10
9
|
if (isErrorWithCode(error, "EPERM")) {
|
|
11
10
|
return {
|
|
12
11
|
status: 403,
|
|
13
12
|
statusText: `EPERM: No permission to read file at ${error.path}`,
|
|
14
13
|
}
|
|
15
14
|
}
|
|
16
|
-
|
|
17
15
|
if (isErrorWithCode(error, "ENOENT")) {
|
|
18
16
|
return {
|
|
19
17
|
status: 404,
|
|
20
18
|
statusText: `ENOENT: File not found at ${error.path}`,
|
|
21
19
|
}
|
|
22
20
|
}
|
|
23
|
-
|
|
24
21
|
// file access may be temporarily blocked
|
|
25
22
|
// (by an antivirus scanning it because recently modified for instance)
|
|
26
23
|
if (isErrorWithCode(error, "EBUSY")) {
|
|
@@ -32,7 +29,6 @@ export const convertFileSystemErrorToResponseProperties = (error) => {
|
|
|
32
29
|
},
|
|
33
30
|
}
|
|
34
31
|
}
|
|
35
|
-
|
|
36
32
|
// emfile means there is too many files currently opened
|
|
37
33
|
if (isErrorWithCode(error, "EMFILE")) {
|
|
38
34
|
return {
|
|
@@ -43,15 +39,13 @@ export const convertFileSystemErrorToResponseProperties = (error) => {
|
|
|
43
39
|
},
|
|
44
40
|
}
|
|
45
41
|
}
|
|
46
|
-
|
|
47
42
|
if (isErrorWithCode(error, "EISDIR")) {
|
|
48
43
|
return {
|
|
49
44
|
status: 500,
|
|
50
45
|
statusText: `EISDIR: Unexpected directory operation at ${error.path}`,
|
|
51
46
|
}
|
|
52
47
|
}
|
|
53
|
-
|
|
54
|
-
return Promise.reject(error)
|
|
48
|
+
return null
|
|
55
49
|
}
|
|
56
50
|
|
|
57
51
|
const isErrorWithCode = (error, code) => {
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { readdirSync } from "node:fs"
|
|
2
|
+
import { pathToFileURL } from "node:url"
|
|
3
|
+
|
|
4
|
+
import { negotiateContentType } from "./content_negotiation/negotiateContentType.js"
|
|
5
|
+
|
|
6
|
+
export const serveDirectory = (
|
|
7
|
+
url,
|
|
8
|
+
{
|
|
9
|
+
headers = {},
|
|
10
|
+
canReadDirectory = false,
|
|
11
|
+
rootDirectoryUrl = `${pathToFileURL(process.cwd())}/`,
|
|
12
|
+
} = {},
|
|
13
|
+
) => {
|
|
14
|
+
if (canReadDirectory === false) {
|
|
15
|
+
return {
|
|
16
|
+
status: 403,
|
|
17
|
+
statusText: "not allowed to read directory",
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
url = String(url)
|
|
21
|
+
url = url[url.length - 1] === "/" ? url : `${url}/`
|
|
22
|
+
const directoryContentArray = readdirSync(new URL(url))
|
|
23
|
+
const responseProducers = {
|
|
24
|
+
"application/json": () => {
|
|
25
|
+
const directoryContentJson = JSON.stringify(directoryContentArray)
|
|
26
|
+
return {
|
|
27
|
+
status: 200,
|
|
28
|
+
headers: {
|
|
29
|
+
"content-type": "application/json",
|
|
30
|
+
"content-length": directoryContentJson.length,
|
|
31
|
+
},
|
|
32
|
+
body: directoryContentJson,
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"text/html": () => {
|
|
36
|
+
const directoryAsHtml = `<!DOCTYPE html>
|
|
37
|
+
<html>
|
|
38
|
+
<head>
|
|
39
|
+
<title>Directory explorer</title>
|
|
40
|
+
<meta charset="utf-8" />
|
|
41
|
+
<link rel="icon" href="data:," />
|
|
42
|
+
</head>
|
|
43
|
+
|
|
44
|
+
<body>
|
|
45
|
+
<h1>Content of directory ${url}</h1>
|
|
46
|
+
<ul>
|
|
47
|
+
${directoryContentArray.map((filename) => {
|
|
48
|
+
const fileUrl = String(new URL(filename, url))
|
|
49
|
+
const fileUrlRelativeToServer = fileUrl.slice(
|
|
50
|
+
String(rootDirectoryUrl).length,
|
|
51
|
+
)
|
|
52
|
+
return `<li>
|
|
53
|
+
<a href="/${fileUrlRelativeToServer}">${fileUrlRelativeToServer}</a>
|
|
54
|
+
</li>`
|
|
55
|
+
}).join(`
|
|
56
|
+
`)}
|
|
57
|
+
</ul>
|
|
58
|
+
</body>
|
|
59
|
+
</html>`
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
status: 200,
|
|
63
|
+
headers: {
|
|
64
|
+
"content-type": "text/html",
|
|
65
|
+
"content-length": Buffer.byteLength(directoryAsHtml),
|
|
66
|
+
},
|
|
67
|
+
body: directoryAsHtml,
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
const bestContentType = negotiateContentType(
|
|
72
|
+
{ headers },
|
|
73
|
+
Object.keys(responseProducers),
|
|
74
|
+
)
|
|
75
|
+
return responseProducers[bestContentType || "application/json"]()
|
|
76
|
+
}
|
package/src/startServer.js
CHANGED
|
@@ -23,7 +23,7 @@ import { composeTwoResponses } from "./internal/response_composition.js"
|
|
|
23
23
|
import { listenRequest } from "./internal/listenRequest.js"
|
|
24
24
|
import { listenServerConnectionError } from "./internal/listenServerConnectionError.js"
|
|
25
25
|
import { memoize } from "./internal/memoize.js"
|
|
26
|
-
import {
|
|
26
|
+
import { applyDefaultErrorToResponse } from "./error_to_response_default.js"
|
|
27
27
|
import {
|
|
28
28
|
STOP_REASON_INTERNAL_ERROR,
|
|
29
29
|
STOP_REASON_PROCESS_SIGHUP,
|
|
@@ -62,8 +62,8 @@ export const startServer = async ({
|
|
|
62
62
|
requestToResponse = () => null,
|
|
63
63
|
plugins = {},
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
sendErrorDetails = false,
|
|
66
|
+
errorToResponse = () => null,
|
|
67
67
|
|
|
68
68
|
onListenStart = () => {},
|
|
69
69
|
onListenEnd = () => {},
|
|
@@ -554,13 +554,15 @@ export const startServer = async ({
|
|
|
554
554
|
// il faudrais pouvoir stop que les autres response ?
|
|
555
555
|
stop(STOP_REASON_INTERNAL_ERROR)
|
|
556
556
|
}
|
|
557
|
-
const
|
|
558
|
-
error,
|
|
559
|
-
{
|
|
557
|
+
const responseDescriptionFromError =
|
|
558
|
+
(await errorToResponse(error, {
|
|
560
559
|
request,
|
|
561
|
-
|
|
562
|
-
}
|
|
563
|
-
|
|
560
|
+
sendErrorDetails,
|
|
561
|
+
})) ||
|
|
562
|
+
(await applyDefaultErrorToResponse(error, {
|
|
563
|
+
request,
|
|
564
|
+
sendErrorDetails,
|
|
565
|
+
}))
|
|
564
566
|
addRequestLog(requestNode, {
|
|
565
567
|
type: "error",
|
|
566
568
|
value: createDetailedMessage(
|
|
@@ -580,7 +582,7 @@ export const startServer = async ({
|
|
|
580
582
|
"content-type": "text/plain",
|
|
581
583
|
},
|
|
582
584
|
},
|
|
583
|
-
|
|
585
|
+
responseDescriptionFromError,
|
|
584
586
|
)
|
|
585
587
|
}
|
|
586
588
|
|