@jsenv/server 12.2.1 → 12.3.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 -0
- package/package.json +13 -13
- package/src/from_fetch_response.js +15 -0
- package/src/internal/nodeStreamToObservable.js +15 -6
- package/src/internal/populateNodeResponse.js +15 -7
- package/src/startServer.js +57 -54
package/main.js
CHANGED
|
@@ -42,6 +42,7 @@ export { pluginContentNegotiationCheck } from "./src/content_negotiation/plugin_
|
|
|
42
42
|
|
|
43
43
|
// others
|
|
44
44
|
export { fetchUrl } from "./src/fetch_url.js"
|
|
45
|
+
export { fromFetchResponse } from "./src/from_fetch_response.js"
|
|
45
46
|
export { composeTwoResponses } from "./src/internal/response_composition.js"
|
|
46
47
|
export { pluginRessourceAliases } from "./src/ressource_aliases/plugin_ressource_aliases.js"
|
|
47
48
|
export { pluginRequestWaitingCheck } from "./src/plugin_request_waiting_check.js"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsenv/server",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.3.3",
|
|
4
4
|
"description": "Write your Node.js server using pure functions",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"url": "https://github.com/jsenv/server"
|
|
14
14
|
},
|
|
15
15
|
"engines": {
|
|
16
|
-
"node": ">=16.
|
|
16
|
+
"node": ">=16.13.0"
|
|
17
17
|
},
|
|
18
18
|
"publishConfig": {
|
|
19
19
|
"access": "public"
|
|
@@ -38,33 +38,33 @@
|
|
|
38
38
|
"test-with-coverage": "npm run test -- --coverage",
|
|
39
39
|
"generate-html-snapshots": "node ./test/internal_error/generate_html_snapshot_files.mjs",
|
|
40
40
|
"prettier": "prettier --write .",
|
|
41
|
-
"certificate-
|
|
41
|
+
"certificate-install": "node ./script/certificate/certificate_install.mjs",
|
|
42
42
|
"playwright-install": "npx playwright install-deps && npx playwright install"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"@jsenv/abort": "4.1.2",
|
|
46
46
|
"@jsenv/logger": "4.0.1",
|
|
47
|
-
"@jsenv/url-meta": "6.0.
|
|
47
|
+
"@jsenv/url-meta": "6.0.3",
|
|
48
48
|
"node-fetch": "2.6.2",
|
|
49
49
|
"path-to-regexp": "6.2.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
-
"@jsenv/assert": "2.4.
|
|
53
|
-
"@jsenv/core": "
|
|
52
|
+
"@jsenv/assert": "2.4.1",
|
|
53
|
+
"@jsenv/core": "25.1.0",
|
|
54
54
|
"@jsenv/eslint-config": "16.0.9",
|
|
55
|
-
"@jsenv/filesystem": "2.6.
|
|
55
|
+
"@jsenv/filesystem": "2.6.1",
|
|
56
56
|
"@jsenv/github-release-package": "1.2.3",
|
|
57
57
|
"@jsenv/https-local": "1.0.3",
|
|
58
|
-
"@jsenv/importmap-eslint-resolver": "5.2.
|
|
59
|
-
"@jsenv/importmap-node-module": "
|
|
58
|
+
"@jsenv/importmap-eslint-resolver": "5.2.2",
|
|
59
|
+
"@jsenv/importmap-node-module": "5.1.0",
|
|
60
60
|
"@jsenv/package-publish": "1.6.2",
|
|
61
61
|
"@jsenv/performance-impact": "2.2.1",
|
|
62
|
-
"eslint": "8.
|
|
63
|
-
"eslint-plugin-import": "2.25.
|
|
62
|
+
"eslint": "8.7.0",
|
|
63
|
+
"eslint-plugin-import": "2.25.4",
|
|
64
64
|
"eventsource": "1.1.0",
|
|
65
|
-
"fastify": "3.25.
|
|
65
|
+
"fastify": "3.25.3",
|
|
66
66
|
"fastify-static": "4.5.0",
|
|
67
|
-
"playwright": "1.17.
|
|
67
|
+
"playwright": "1.17.2",
|
|
68
68
|
"prettier": "2.5.1"
|
|
69
69
|
}
|
|
70
70
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const fromFetchResponse = (fetchResponse) => {
|
|
2
|
+
const responseHeaders = {}
|
|
3
|
+
const headersToIgnore = ["connection"]
|
|
4
|
+
fetchResponse.headers.forEach((value, name) => {
|
|
5
|
+
if (!headersToIgnore.includes(name)) {
|
|
6
|
+
responseHeaders[name] = value
|
|
7
|
+
}
|
|
8
|
+
})
|
|
9
|
+
return {
|
|
10
|
+
status: fetchResponse.status,
|
|
11
|
+
statusText: fetchResponse.statusText,
|
|
12
|
+
headers: responseHeaders,
|
|
13
|
+
body: fetchResponse.body, // node-fetch assumed
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
// https://github.com/jamestalmage/stream-to-observable/blob/master/index.js
|
|
2
2
|
import { Readable } from "node:stream"
|
|
3
|
-
|
|
4
3
|
import { createObservable } from "./observable.js"
|
|
5
4
|
|
|
6
5
|
const readableStreamLifetimeInSeconds = 120
|
|
7
6
|
|
|
8
7
|
export const nodeStreamToObservable = (nodeStream) => {
|
|
9
8
|
const observable = createObservable(({ next, error, complete }) => {
|
|
9
|
+
if (nodeStream.isPaused()) {
|
|
10
|
+
nodeStream.resume()
|
|
11
|
+
} else if (nodeStream.complete) {
|
|
12
|
+
complete()
|
|
13
|
+
return null
|
|
14
|
+
}
|
|
10
15
|
const cleanup = () => {
|
|
11
16
|
nodeStream.removeListener("data", next)
|
|
12
17
|
nodeStream.removeListener("error", error)
|
|
@@ -14,13 +19,17 @@ export const nodeStreamToObservable = (nodeStream) => {
|
|
|
14
19
|
nodeStream.removeListener("close", cleanup)
|
|
15
20
|
nodeStream.destroy()
|
|
16
21
|
}
|
|
17
|
-
|
|
18
22
|
// should we do nodeStream.resume() in case the stream was paused ?
|
|
19
23
|
nodeStream.once("error", error)
|
|
20
|
-
nodeStream.on("data",
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
nodeStream.on("data", (data) => {
|
|
25
|
+
next(data)
|
|
26
|
+
})
|
|
27
|
+
nodeStream.once("close", () => {
|
|
28
|
+
cleanup()
|
|
29
|
+
})
|
|
30
|
+
nodeStream.once("end", () => {
|
|
31
|
+
complete()
|
|
32
|
+
})
|
|
24
33
|
return cleanup
|
|
25
34
|
})
|
|
26
35
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import http from "node:http"
|
|
1
2
|
import { Http2ServerResponse } from "node:http2"
|
|
2
3
|
import { raceCallbacks } from "@jsenv/abort"
|
|
3
4
|
|
|
@@ -22,8 +23,8 @@ export const populateNodeResponse = async (
|
|
|
22
23
|
status,
|
|
23
24
|
statusText,
|
|
24
25
|
headers,
|
|
26
|
+
onHeadersSent,
|
|
25
27
|
})
|
|
26
|
-
onHeadersSent()
|
|
27
28
|
|
|
28
29
|
if (!body) {
|
|
29
30
|
onEnd()
|
|
@@ -134,41 +135,48 @@ export const populateNodeResponse = async (
|
|
|
134
135
|
})
|
|
135
136
|
}
|
|
136
137
|
|
|
137
|
-
const writeHead = (
|
|
138
|
+
const writeHead = (
|
|
139
|
+
responseStream,
|
|
140
|
+
{ status, statusText, headers, onHeadersSent },
|
|
141
|
+
) => {
|
|
138
142
|
const responseIsHttp2ServerResponse =
|
|
139
143
|
responseStream instanceof Http2ServerResponse
|
|
140
144
|
const responseIsServerHttp2Stream =
|
|
141
145
|
responseStream.constructor.name === "ServerHttp2Stream"
|
|
142
|
-
|
|
143
146
|
let nodeHeaders = headersToNodeHeaders(headers, {
|
|
144
147
|
// https://github.com/nodejs/node/blob/79296dc2d02c0b9872bbfcbb89148ea036a546d0/lib/internal/http2/compat.js#L112
|
|
145
148
|
ignoreConnectionHeader:
|
|
146
149
|
responseIsHttp2ServerResponse || responseIsServerHttp2Stream,
|
|
147
150
|
})
|
|
148
|
-
|
|
151
|
+
if (statusText === undefined) {
|
|
152
|
+
statusText = statusTextFromStatus(status)
|
|
153
|
+
}
|
|
149
154
|
if (responseIsServerHttp2Stream) {
|
|
150
155
|
nodeHeaders = {
|
|
151
156
|
...nodeHeaders,
|
|
152
157
|
":status": status,
|
|
153
158
|
}
|
|
154
159
|
responseStream.respond(nodeHeaders)
|
|
160
|
+
onHeadersSent({ nodeHeaders, status, statusText })
|
|
155
161
|
return
|
|
156
162
|
}
|
|
157
|
-
|
|
158
163
|
// nodejs strange signature for writeHead force this
|
|
159
164
|
// https://nodejs.org/api/http.html#http_response_writehead_statuscode_statusmessage_headers
|
|
160
165
|
if (
|
|
161
|
-
statusText === undefined ||
|
|
162
166
|
// https://github.com/nodejs/node/blob/79296dc2d02c0b9872bbfcbb89148ea036a546d0/lib/internal/http2/compat.js#L97
|
|
163
167
|
responseIsHttp2ServerResponse
|
|
164
168
|
) {
|
|
165
169
|
responseStream.writeHead(status, nodeHeaders)
|
|
170
|
+
onHeadersSent({ nodeHeaders, status, statusText })
|
|
166
171
|
return
|
|
167
172
|
}
|
|
168
|
-
|
|
169
173
|
responseStream.writeHead(status, statusText, nodeHeaders)
|
|
174
|
+
onHeadersSent({ nodeHeaders, status, statusText })
|
|
170
175
|
}
|
|
171
176
|
|
|
177
|
+
const statusTextFromStatus = (status) =>
|
|
178
|
+
http.STATUS_CODES[status] || "not specified"
|
|
179
|
+
|
|
172
180
|
const headersToNodeHeaders = (headers, { ignoreConnectionHeader }) => {
|
|
173
181
|
const nodeHeaders = {}
|
|
174
182
|
|
package/src/startServer.js
CHANGED
|
@@ -230,6 +230,10 @@ export const startServer = async ({
|
|
|
230
230
|
})
|
|
231
231
|
|
|
232
232
|
const requestCallback = async (nodeRequest, nodeResponse) => {
|
|
233
|
+
// pause the stream to let a chance to "requestToResponse"
|
|
234
|
+
// to call "requestRequestBody". Without this the request body readable stream
|
|
235
|
+
// might be closed when we'll try to attach "data" and "end" listeners to it
|
|
236
|
+
nodeRequest.pause()
|
|
233
237
|
if (!nagle) {
|
|
234
238
|
nodeRequest.connection.setNoDelay(true)
|
|
235
239
|
}
|
|
@@ -309,7 +313,6 @@ export const startServer = async ({
|
|
|
309
313
|
|
|
310
314
|
visitRequestNodeToLog(rootRequestNode, 0)
|
|
311
315
|
}
|
|
312
|
-
|
|
313
316
|
nodeRequest.on("error", (error) => {
|
|
314
317
|
if (error.message === "aborted") {
|
|
315
318
|
addRequestLog(rootRequestNode, {
|
|
@@ -472,8 +475,8 @@ export const startServer = async ({
|
|
|
472
475
|
}
|
|
473
476
|
}
|
|
474
477
|
})
|
|
475
|
-
|
|
476
|
-
? {
|
|
478
|
+
const result = responseFromShortcircuit
|
|
479
|
+
? { returnValue: responseFromShortcircuit }
|
|
477
480
|
: await generateResponseDescription({
|
|
478
481
|
requestToResponse,
|
|
479
482
|
request,
|
|
@@ -533,65 +536,72 @@ export const startServer = async ({
|
|
|
533
536
|
},
|
|
534
537
|
})
|
|
535
538
|
|
|
539
|
+
const { aborted } = result
|
|
536
540
|
if (aborted) {
|
|
537
541
|
return ABORTED_RESPONSE_PROPERTIES
|
|
538
542
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
if (
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
{
|
|
559
|
-
status: 500,
|
|
560
|
-
statusText: statusToStatusText(500),
|
|
561
|
-
headers: {
|
|
562
|
-
// ensure error are not cached
|
|
563
|
-
"cache-control": "no-store",
|
|
564
|
-
"content-type": "text/plain",
|
|
543
|
+
const readResponseProperties = async () => {
|
|
544
|
+
const { error } = result
|
|
545
|
+
// internal error, create 500 response
|
|
546
|
+
if (error) {
|
|
547
|
+
if (
|
|
548
|
+
// stopOnInternalError stops server only if requestToResponse generated
|
|
549
|
+
// a non controlled error (internal error).
|
|
550
|
+
// if requestToResponse gracefully produced a 500 response (it did not throw)
|
|
551
|
+
// then we can assume we are still in control of what we are doing
|
|
552
|
+
stopOnInternalError
|
|
553
|
+
) {
|
|
554
|
+
// il faudrais pouvoir stop que les autres response ?
|
|
555
|
+
stop(STOP_REASON_INTERNAL_ERROR)
|
|
556
|
+
}
|
|
557
|
+
const propertiesFromError = await serverInternalErrorToResponse(
|
|
558
|
+
error,
|
|
559
|
+
{
|
|
560
|
+
request,
|
|
561
|
+
sendServerInternalErrorDetails,
|
|
565
562
|
},
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
563
|
+
)
|
|
564
|
+
addRequestLog(requestNode, {
|
|
565
|
+
type: "error",
|
|
566
|
+
value: createDetailedMessage(
|
|
567
|
+
`internal error while handling request`,
|
|
568
|
+
{
|
|
569
|
+
"error stack": error.stack,
|
|
570
|
+
},
|
|
571
|
+
),
|
|
572
|
+
})
|
|
573
|
+
return composeTwoResponses(
|
|
573
574
|
{
|
|
574
|
-
|
|
575
|
+
status: 500,
|
|
576
|
+
statusText: "Internal Server Error",
|
|
577
|
+
headers: {
|
|
578
|
+
// ensure error are not cached
|
|
579
|
+
"cache-control": "no-store",
|
|
580
|
+
"content-type": "text/plain",
|
|
581
|
+
},
|
|
575
582
|
},
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
583
|
+
propertiesFromError,
|
|
584
|
+
)
|
|
585
|
+
}
|
|
586
|
+
|
|
579
587
|
const {
|
|
580
588
|
status = 501,
|
|
581
|
-
statusText
|
|
589
|
+
statusText,
|
|
582
590
|
headers = {},
|
|
583
591
|
body,
|
|
584
592
|
...rest
|
|
585
|
-
} =
|
|
586
|
-
responseProperties = {
|
|
593
|
+
} = result.returnValue || {}
|
|
594
|
+
const responseProperties = {
|
|
587
595
|
status,
|
|
588
596
|
statusText,
|
|
589
597
|
headers,
|
|
590
598
|
body,
|
|
591
599
|
...rest,
|
|
592
600
|
}
|
|
601
|
+
return responseProperties
|
|
593
602
|
}
|
|
594
603
|
|
|
604
|
+
let responseProperties = await readResponseProperties()
|
|
595
605
|
// call every plugin "onResponse" hook
|
|
596
606
|
requestPlugins.forEach((requestPlugin) => {
|
|
597
607
|
const { onResponse } = requestPlugin
|
|
@@ -605,7 +615,6 @@ export const startServer = async ({
|
|
|
605
615
|
}
|
|
606
616
|
}
|
|
607
617
|
})
|
|
608
|
-
|
|
609
618
|
if (
|
|
610
619
|
request.method !== "HEAD" &&
|
|
611
620
|
responseProperties.headers["content-length"] > 0 &&
|
|
@@ -616,7 +625,6 @@ export const startServer = async ({
|
|
|
616
625
|
value: `content-length header is ${responseProperties.headers["content-length"]} but body is empty`,
|
|
617
626
|
})
|
|
618
627
|
}
|
|
619
|
-
|
|
620
628
|
return responseProperties
|
|
621
629
|
}
|
|
622
630
|
|
|
@@ -659,12 +667,10 @@ export const startServer = async ({
|
|
|
659
667
|
})
|
|
660
668
|
onRequestHandled(requestNode)
|
|
661
669
|
},
|
|
662
|
-
onHeadersSent: () => {
|
|
670
|
+
onHeadersSent: ({ status, statusText }) => {
|
|
663
671
|
addRequestLog(requestNode, {
|
|
664
672
|
type: "info",
|
|
665
|
-
value: `${colorizeResponseStatus(
|
|
666
|
-
responseProperties.statusText
|
|
667
|
-
}`,
|
|
673
|
+
value: `${colorizeResponseStatus(status)} ${statusText}`,
|
|
668
674
|
})
|
|
669
675
|
},
|
|
670
676
|
onEnd: () => {
|
|
@@ -680,6 +686,7 @@ export const startServer = async ({
|
|
|
680
686
|
const responseProperties = await handleRequest(request, {
|
|
681
687
|
requestNode: rootRequestNode,
|
|
682
688
|
})
|
|
689
|
+
nodeRequest.resume()
|
|
683
690
|
if (receiveRequestOperation.signal.aborted) {
|
|
684
691
|
return
|
|
685
692
|
}
|
|
@@ -759,7 +766,7 @@ const generateResponseDescription = async ({
|
|
|
759
766
|
pushResponse,
|
|
760
767
|
})
|
|
761
768
|
return {
|
|
762
|
-
|
|
769
|
+
returnValue,
|
|
763
770
|
}
|
|
764
771
|
} catch (error) {
|
|
765
772
|
if (error.name === "AbortError" && request.signal.aborted) {
|
|
@@ -772,10 +779,6 @@ const generateResponseDescription = async ({
|
|
|
772
779
|
}
|
|
773
780
|
}
|
|
774
781
|
}
|
|
775
|
-
|
|
776
|
-
const statusToStatusText = (status) =>
|
|
777
|
-
http.STATUS_CODES[status] || "not specified"
|
|
778
|
-
|
|
779
782
|
const testCanPushStream = (http2Stream) => {
|
|
780
783
|
if (!http2Stream.pushAllowed) {
|
|
781
784
|
return {
|