@jsenv/server 14.1.3 → 14.1.5
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/package.json +1 -1
- package/src/{internal → interfacing_with_node}/body.js +3 -3
- package/src/{internal → interfacing_with_node}/observable.js +0 -0
- package/src/{internal/nodeStreamToObservable.js → interfacing_with_node/observable_from_node_stream.js} +10 -5
- package/src/{internal → interfacing_with_node}/request_factory.js +9 -4
- package/src/{internal/populateNodeResponse.js → interfacing_with_node/write_node_response.js} +15 -2
- package/src/sse/sse_room.js +1 -1
- package/src/start_server.js +106 -74
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@ import { Stream, Writable, Readable } from "node:stream"
|
|
|
2
2
|
import { createReadStream } from "node:fs"
|
|
3
3
|
|
|
4
4
|
import { isObservable, observableFromValue } from "./observable.js"
|
|
5
|
-
import {
|
|
5
|
+
import { observableFromNodeStream } from "./observable_from_node_stream.js"
|
|
6
6
|
|
|
7
7
|
export const normalizeBodyMethods = (body) => {
|
|
8
8
|
if (isObservable(body)) {
|
|
@@ -23,7 +23,7 @@ export const normalizeBodyMethods = (body) => {
|
|
|
23
23
|
|
|
24
24
|
if (isNodeStream(body)) {
|
|
25
25
|
return {
|
|
26
|
-
asObservable: () =>
|
|
26
|
+
asObservable: () => observableFromNodeStream(body),
|
|
27
27
|
destroy: () => {
|
|
28
28
|
body.destroy()
|
|
29
29
|
},
|
|
@@ -61,7 +61,7 @@ export const fileHandleToReadableStream = (fileHandle) => {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
const fileHandleToObservable = (fileHandle) => {
|
|
64
|
-
return
|
|
64
|
+
return observableFromNodeStream(fileHandleToReadableStream(fileHandle))
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
const isNodeStream = (value) => {
|
|
File without changes
|
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
import { Readable } from "node:stream"
|
|
3
3
|
import { createObservable } from "./observable.js"
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
export const observableFromNodeStream = (
|
|
6
|
+
nodeStream,
|
|
7
|
+
{
|
|
8
|
+
readableStreamLifetime = 120_000, // 2s
|
|
9
|
+
} = {},
|
|
10
|
+
) => {
|
|
8
11
|
const observable = createObservable(({ next, error, complete }) => {
|
|
9
12
|
if (nodeStream.isPaused()) {
|
|
10
13
|
nodeStream.resume()
|
|
@@ -38,7 +41,9 @@ export const nodeStreamToObservable = (nodeStream) => {
|
|
|
38
41
|
// used in the next ${readableStreamLifetimeInSeconds} otherwise destroys it
|
|
39
42
|
const timeout = setTimeout(() => {
|
|
40
43
|
process.emitWarning(
|
|
41
|
-
`Readable stream not used after ${
|
|
44
|
+
`Readable stream not used after ${
|
|
45
|
+
readableStreamLifetime / 1000
|
|
46
|
+
} seconds. It will be destroyed to release resources`,
|
|
42
47
|
{
|
|
43
48
|
CODE: "READABLE_STREAM_TIMEOUT",
|
|
44
49
|
// url is for http client request
|
|
@@ -46,7 +51,7 @@ export const nodeStreamToObservable = (nodeStream) => {
|
|
|
46
51
|
},
|
|
47
52
|
)
|
|
48
53
|
nodeStream.destroy()
|
|
49
|
-
},
|
|
54
|
+
}, readableStreamLifetime)
|
|
50
55
|
observable.timeout = timeout
|
|
51
56
|
onceReadableStreamUsedOrClosed(nodeStream, () => {
|
|
52
57
|
clearTimeout(timeout)
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { headersFromObject } from "
|
|
1
|
+
import { observableFromNodeStream } from "./observable_from_node_stream.js"
|
|
2
|
+
import { headersFromObject } from "../internal/headersFromObject.js"
|
|
3
3
|
|
|
4
|
-
export const fromNodeRequest = (
|
|
4
|
+
export const fromNodeRequest = (
|
|
5
|
+
nodeRequest,
|
|
6
|
+
{ serverOrigin, signal, requestBodyLifetime },
|
|
7
|
+
) => {
|
|
5
8
|
const headers = headersFromObject(nodeRequest.headers)
|
|
6
|
-
const body =
|
|
9
|
+
const body = observableFromNodeStream(nodeRequest, {
|
|
10
|
+
readableStreamLifetime: requestBodyLifetime,
|
|
11
|
+
})
|
|
7
12
|
|
|
8
13
|
let requestOrigin
|
|
9
14
|
if (nodeRequest.upgrade) {
|
package/src/{internal/populateNodeResponse.js → interfacing_with_node/write_node_response.js}
RENAMED
|
@@ -4,7 +4,7 @@ import { raceCallbacks } from "@jsenv/abort"
|
|
|
4
4
|
|
|
5
5
|
import { normalizeBodyMethods } from "./body.js"
|
|
6
6
|
|
|
7
|
-
export const
|
|
7
|
+
export const writeNodeResponse = async (
|
|
8
8
|
responseStream,
|
|
9
9
|
{ status, statusText, headers, body, bodyEncoding },
|
|
10
10
|
{ signal, ignoreBody, onAbort, onError, onHeadersSent, onEnd } = {},
|
|
@@ -170,7 +170,20 @@ const writeHead = (
|
|
|
170
170
|
onHeadersSent({ nodeHeaders, status, statusText })
|
|
171
171
|
return
|
|
172
172
|
}
|
|
173
|
-
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
responseStream.writeHead(status, statusText, nodeHeaders)
|
|
176
|
+
} catch (e) {
|
|
177
|
+
if (
|
|
178
|
+
e.code === "ERR_INVALID_CHAR" &&
|
|
179
|
+
e.message.includes("Invalid character in statusMessage")
|
|
180
|
+
) {
|
|
181
|
+
throw new Error(`Invalid character in statusMessage
|
|
182
|
+
--- status message ---
|
|
183
|
+
${statusText}`)
|
|
184
|
+
}
|
|
185
|
+
throw e
|
|
186
|
+
}
|
|
174
187
|
onHeadersSent({ nodeHeaders, status, statusText })
|
|
175
188
|
}
|
|
176
189
|
|
package/src/sse/sse_room.js
CHANGED
|
@@ -3,7 +3,7 @@ import { createLogger } from "@jsenv/log"
|
|
|
3
3
|
import {
|
|
4
4
|
createObservable,
|
|
5
5
|
createCompositeProducer,
|
|
6
|
-
} from "@jsenv/server/src/
|
|
6
|
+
} from "@jsenv/server/src/interfacing_with_node/observable.js"
|
|
7
7
|
|
|
8
8
|
// https://www.html5rocks.com/en/tutorials/eventsource/basics/
|
|
9
9
|
export const createSSERoom = ({
|
package/src/start_server.js
CHANGED
|
@@ -18,8 +18,8 @@ import {
|
|
|
18
18
|
fromNodeRequest,
|
|
19
19
|
createPushRequest,
|
|
20
20
|
applyRedirectionToRequest,
|
|
21
|
-
} from "./
|
|
22
|
-
import {
|
|
21
|
+
} from "./interfacing_with_node/request_factory.js"
|
|
22
|
+
import { writeNodeResponse } from "./interfacing_with_node/write_node_response.js"
|
|
23
23
|
import {
|
|
24
24
|
statusToType,
|
|
25
25
|
colorizeResponseStatus,
|
|
@@ -87,6 +87,13 @@ export const startServer = async ({
|
|
|
87
87
|
),
|
|
88
88
|
)
|
|
89
89
|
},
|
|
90
|
+
// timeAllocated to start responding to a request
|
|
91
|
+
// after this delay the server will respond with 504
|
|
92
|
+
responseTimeout = 60_000 * 10, // 10s
|
|
93
|
+
// time allocated to server code to start reading the request body
|
|
94
|
+
// after this delay the underlying stream is destroyed, attempting to read it would throw
|
|
95
|
+
// if used the stream stays opened, it's only if the stream is not read at all that it gets destroyed
|
|
96
|
+
requestBodyLifetime = 60_000 * 2, // 2s
|
|
90
97
|
} = {}) => {
|
|
91
98
|
const logger = createLogger({ logLevel })
|
|
92
99
|
|
|
@@ -207,6 +214,7 @@ export const startServer = async ({
|
|
|
207
214
|
const firstExternalIp = ipGetters.getFirstExternalIp({ preferIpv6 })
|
|
208
215
|
serverOrigins.externalip = createOrigin(firstExternalIp)
|
|
209
216
|
} else if (hostnameInfo.label === "loopback") {
|
|
217
|
+
// nothing
|
|
210
218
|
} else {
|
|
211
219
|
serverOrigins.local = createOrigin(hostname)
|
|
212
220
|
}
|
|
@@ -534,7 +542,8 @@ export const startServer = async ({
|
|
|
534
542
|
abortController.abort()
|
|
535
543
|
} else if (!http2Stream.pushAllowed) {
|
|
536
544
|
abortController.abort()
|
|
537
|
-
} else if (responseProperties
|
|
545
|
+
} else if (responseProperties.requestAborted) {
|
|
546
|
+
} else {
|
|
538
547
|
const responseLength =
|
|
539
548
|
responseProperties.headers["content-length"] || 0
|
|
540
549
|
const { effectiveRecvDataLength, remoteWindowSize } =
|
|
@@ -608,75 +617,95 @@ export const startServer = async ({
|
|
|
608
617
|
let handleRequestReturnValue
|
|
609
618
|
let errorWhileHandlingRequest = null
|
|
610
619
|
let handleRequestTimings = serverTiming ? {} : null
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
{ path, method },
|
|
669
|
-
{
|
|
670
|
-
requestNode: requestChildNode,
|
|
671
|
-
parentHttp2Stream: nodeResponse.stream,
|
|
672
|
-
},
|
|
673
|
-
)
|
|
620
|
+
|
|
621
|
+
let timeout
|
|
622
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
623
|
+
timeout = setTimeout(() => {
|
|
624
|
+
resolve({
|
|
625
|
+
// the correct status code should be 500 because it's
|
|
626
|
+
// we don't really know what takes time
|
|
627
|
+
// in practice it's often because server is trying to reach an other server
|
|
628
|
+
// that is not responding so 504 is more correct
|
|
629
|
+
status: 504,
|
|
630
|
+
statusText: `server timeout after ${
|
|
631
|
+
responseTimeout / 1000
|
|
632
|
+
}s waiting to handle request`,
|
|
633
|
+
})
|
|
634
|
+
}, responseTimeout)
|
|
635
|
+
})
|
|
636
|
+
const handleRequestPromise = serviceController.callAsyncHooksUntil(
|
|
637
|
+
"handleRequest",
|
|
638
|
+
request,
|
|
639
|
+
{
|
|
640
|
+
timing: handleRequestTimings,
|
|
641
|
+
warn,
|
|
642
|
+
pushResponse: async ({ path, method }) => {
|
|
643
|
+
if (typeof path !== "string" || path[0] !== "/") {
|
|
644
|
+
addRequestLog(requestNode, {
|
|
645
|
+
type: "warn",
|
|
646
|
+
value: `response push ignored because path is invalid (must be a string starting with "/", found ${path})`,
|
|
647
|
+
})
|
|
648
|
+
return
|
|
649
|
+
}
|
|
650
|
+
if (!request.http2) {
|
|
651
|
+
addRequestLog(requestNode, {
|
|
652
|
+
type: "warn",
|
|
653
|
+
value: `response push ignored because request is not http2`,
|
|
654
|
+
})
|
|
655
|
+
return
|
|
656
|
+
}
|
|
657
|
+
const canPushStream = testCanPushStream(nodeResponse.stream)
|
|
658
|
+
if (!canPushStream.can) {
|
|
659
|
+
addRequestLog(requestNode, {
|
|
660
|
+
type: "debug",
|
|
661
|
+
value: `response push ignored because ${canPushStream.reason}`,
|
|
662
|
+
})
|
|
663
|
+
return
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
let preventedByService = null
|
|
667
|
+
const prevent = () => {
|
|
668
|
+
preventedByService = serviceController.getCurrentService()
|
|
669
|
+
}
|
|
670
|
+
serviceController.callHooksUntil(
|
|
671
|
+
"onResponsePush",
|
|
672
|
+
{ path, method },
|
|
673
|
+
{
|
|
674
|
+
request,
|
|
675
|
+
warn,
|
|
676
|
+
prevent,
|
|
674
677
|
},
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
678
|
+
() => preventedByService,
|
|
679
|
+
)
|
|
680
|
+
if (preventedByService) {
|
|
681
|
+
addRequestLog(requestNode, {
|
|
682
|
+
type: "debug",
|
|
683
|
+
value: `response push prevented by "${preventedByService.name}" service`,
|
|
684
|
+
})
|
|
685
|
+
return
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const requestChildNode = { logs: [], children: [] }
|
|
689
|
+
requestNode.children.push(requestChildNode)
|
|
690
|
+
await pushResponse(
|
|
691
|
+
{ path, method },
|
|
692
|
+
{
|
|
693
|
+
requestNode: requestChildNode,
|
|
694
|
+
parentHttp2Stream: nodeResponse.stream,
|
|
695
|
+
},
|
|
696
|
+
)
|
|
697
|
+
},
|
|
698
|
+
},
|
|
699
|
+
)
|
|
700
|
+
try {
|
|
701
|
+
handleRequestReturnValue = await Promise.race([
|
|
702
|
+
timeoutPromise,
|
|
703
|
+
handleRequestPromise,
|
|
704
|
+
])
|
|
705
|
+
} catch (e) {
|
|
706
|
+
errorWhileHandlingRequest = e
|
|
679
707
|
}
|
|
708
|
+
clearTimeout(timeout)
|
|
680
709
|
|
|
681
710
|
let responseProperties
|
|
682
711
|
if (errorWhileHandlingRequest) {
|
|
@@ -684,7 +713,7 @@ export const startServer = async ({
|
|
|
684
713
|
errorWhileHandlingRequest.name === "AbortError" &&
|
|
685
714
|
request.signal.aborted
|
|
686
715
|
) {
|
|
687
|
-
responseProperties =
|
|
716
|
+
responseProperties = { requestAborted: true }
|
|
688
717
|
} else {
|
|
689
718
|
// internal error, create 500 response
|
|
690
719
|
if (
|
|
@@ -817,7 +846,7 @@ export const startServer = async ({
|
|
|
817
846
|
await new Promise((resolve) => setTimeout(resolve))
|
|
818
847
|
}
|
|
819
848
|
|
|
820
|
-
await
|
|
849
|
+
await writeNodeResponse(responseStream, responseProperties, {
|
|
821
850
|
signal,
|
|
822
851
|
ignoreBody,
|
|
823
852
|
onAbort: () => {
|
|
@@ -926,6 +955,7 @@ export const startServer = async ({
|
|
|
926
955
|
const request = fromNodeRequest(nodeRequest, {
|
|
927
956
|
serverOrigin: websocketOrigin,
|
|
928
957
|
signal: new AbortController().signal,
|
|
958
|
+
requestBodyLifetime,
|
|
929
959
|
})
|
|
930
960
|
serviceController.callAsyncHooksUntil(
|
|
931
961
|
"handleWebsocket",
|
|
@@ -947,6 +977,10 @@ export const startServer = async ({
|
|
|
947
977
|
)
|
|
948
978
|
stopCallbackList.add(removeUpgradeCallback)
|
|
949
979
|
stopCallbackList.add(() => {
|
|
980
|
+
websocketClients.forEach((websocketClient) => {
|
|
981
|
+
websocketClient.close()
|
|
982
|
+
})
|
|
983
|
+
websocketClients.clear()
|
|
950
984
|
websocketServer.close()
|
|
951
985
|
websocketServer = null
|
|
952
986
|
})
|
|
@@ -1034,8 +1068,6 @@ const testCanPushStream = (http2Stream) => {
|
|
|
1034
1068
|
}
|
|
1035
1069
|
}
|
|
1036
1070
|
|
|
1037
|
-
const ABORTED_RESPONSE_PROPERTIES = {}
|
|
1038
|
-
|
|
1039
1071
|
const PROCESS_TEARDOWN_EVENTS_MAP = {
|
|
1040
1072
|
SIGHUP: STOP_REASON_PROCESS_SIGHUP,
|
|
1041
1073
|
SIGTERM: STOP_REASON_PROCESS_SIGTERM,
|