@jsenv/server 13.0.0 → 14.1.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/src/server.js DELETED
@@ -1,896 +0,0 @@
1
- import http from "node:http"
2
- import cluster from "node:cluster"
3
- import { createDetailedMessage, createLogger } from "@jsenv/log"
4
- import {
5
- Abort,
6
- raceProcessTeardownEvents,
7
- createCallbackListNotifiedOnce,
8
- } from "@jsenv/abort"
9
- import { memoize } from "@jsenv/utils/src/memoize/memoize.js"
10
-
11
- import { createServiceController } from "./service_controller.js"
12
- import { timingToServerTimingResponseHeaders } from "./server_timing/timing_header.js"
13
- import { createPolyglotServer } from "./internal/server-polyglot.js"
14
- import { trackServerPendingConnections } from "./internal/trackServerPendingConnections.js"
15
- import { trackServerPendingRequests } from "./internal/trackServerPendingRequests.js"
16
- import {
17
- fromNodeRequest,
18
- createPushRequest,
19
- applyRedirectionToRequest,
20
- } from "./internal/request_factory.js"
21
- import { populateNodeResponse } from "./internal/populateNodeResponse.js"
22
- import {
23
- statusToType,
24
- colorizeResponseStatus,
25
- } from "./internal/colorizeResponseStatus.js"
26
- import { getServerOrigins } from "./internal/getServerOrigins.js"
27
- import { listen, stopListening } from "./internal/listen.js"
28
- import { composeTwoResponses } from "./internal/response_composition.js"
29
- import { listenRequest } from "./internal/listenRequest.js"
30
- import { listenServerConnectionError } from "./internal/listenServerConnectionError.js"
31
- import {
32
- STOP_REASON_INTERNAL_ERROR,
33
- STOP_REASON_PROCESS_SIGHUP,
34
- STOP_REASON_PROCESS_SIGTERM,
35
- STOP_REASON_PROCESS_SIGINT,
36
- STOP_REASON_PROCESS_BEFORE_EXIT,
37
- STOP_REASON_PROCESS_EXIT,
38
- STOP_REASON_NOT_SPECIFIED,
39
- } from "./stopReasons.js"
40
- import { composeTwoHeaders } from "./internal/headers_composition.js"
41
-
42
- export const startServer = async ({
43
- signal = new AbortController().signal,
44
- logLevel,
45
- startLog = true,
46
- serverName = "server",
47
-
48
- protocol = "http",
49
- http2 = false,
50
- http1Allowed = true,
51
- redirectHttpToHttps,
52
- allowHttpRequestOnHttps = false,
53
- acceptAnyIp = false,
54
- host = acceptAnyIp ? undefined : "localhost",
55
- port = 0, // assign a random available port
56
- portHint,
57
- privateKey,
58
- certificate,
59
-
60
- // when inside a worker, we should not try to stop server on SIGINT
61
- // otherwise it can create an EPIPE error while primary process tries
62
- // to kill the server
63
- stopOnSIGINT = !cluster.isWorker,
64
- // auto close the server when the process exits
65
- stopOnExit = true,
66
- // auto close when requestToResponse throw an error
67
- stopOnInternalError = false,
68
- keepProcessAlive = true,
69
- services = [],
70
- nagle = true,
71
- serverTiming = false,
72
- requestWaitingMs = 0,
73
- requestWaitingCallback = ({ request, warn, requestWaitingMs }) => {
74
- warn(
75
- createDetailedMessage(
76
- `still no response found for request after ${requestWaitingMs} ms`,
77
- {
78
- "request url": `${request.origin}${request.ressource}`,
79
- "request headers": JSON.stringify(request.headers, null, " "),
80
- },
81
- ),
82
- )
83
- },
84
- } = {}) => {
85
- if (protocol !== "http" && protocol !== "https") {
86
- throw new Error(`protocol must be http or https, got ${protocol}`)
87
- }
88
- if (protocol === "https") {
89
- if (!certificate) {
90
- throw new Error(`missing certificate for https server`)
91
- }
92
- if (!privateKey) {
93
- throw new Error(`missing privateKey for https server`)
94
- }
95
- }
96
- if (http2 && protocol !== "https") {
97
- throw new Error(`http2 needs "https" but protocol is "${protocol}"`)
98
- }
99
-
100
- const logger = createLogger({ logLevel })
101
- if (
102
- redirectHttpToHttps === undefined &&
103
- protocol === "https" &&
104
- !allowHttpRequestOnHttps
105
- ) {
106
- redirectHttpToHttps = true
107
- }
108
- if (redirectHttpToHttps && protocol === "http") {
109
- logger.warn(`redirectHttpToHttps ignored because protocol is http`)
110
- redirectHttpToHttps = false
111
- }
112
- if (allowHttpRequestOnHttps && redirectHttpToHttps) {
113
- logger.warn(
114
- `redirectHttpToHttps ignored because allowHttpRequestOnHttps is enabled`,
115
- )
116
- redirectHttpToHttps = false
117
- }
118
-
119
- if (allowHttpRequestOnHttps && protocol === "http") {
120
- logger.warn(`allowHttpRequestOnHttps ignored because protocol is http`)
121
- allowHttpRequestOnHttps = false
122
- }
123
-
124
- const serviceController = createServiceController(services)
125
-
126
- const processTeardownEvents = {
127
- SIGHUP: stopOnExit,
128
- SIGTERM: stopOnExit,
129
- SIGINT: stopOnSIGINT,
130
- beforeExit: stopOnExit,
131
- exit: stopOnExit,
132
- }
133
-
134
- let status = "starting"
135
- let nodeServer
136
- const startServerOperation = Abort.startOperation()
137
-
138
- try {
139
- startServerOperation.addAbortSignal(signal)
140
- startServerOperation.addAbortSource((abort) => {
141
- return raceProcessTeardownEvents(processTeardownEvents, ({ name }) => {
142
- logger.info(`process teardown (${name}) -> aborting start server`)
143
- abort()
144
- })
145
- })
146
- startServerOperation.throwIfAborted()
147
- nodeServer = await createNodeServer({
148
- protocol,
149
- redirectHttpToHttps,
150
- allowHttpRequestOnHttps,
151
- certificate,
152
- privateKey,
153
- http2,
154
- http1Allowed,
155
- })
156
- startServerOperation.throwIfAborted()
157
-
158
- // https://nodejs.org/api/net.html#net_server_unref
159
- if (!keepProcessAlive) {
160
- nodeServer.unref()
161
- }
162
-
163
- port = await listen({
164
- signal: startServerOperation.signal,
165
- server: nodeServer,
166
- port,
167
- portHint,
168
- host,
169
- })
170
- serviceController.callHooks("serverListening", { port })
171
- startServerOperation.addAbortCallback(async () => {
172
- await stopListening(nodeServer)
173
- })
174
- startServerOperation.throwIfAborted()
175
- } finally {
176
- await startServerOperation.end()
177
- }
178
-
179
- // now the server is started (listening) it cannot be aborted anymore
180
- // (otherwise an AbortError is thrown to the code calling "startServer")
181
- // we can proceed to create a stop function to stop it gacefully
182
- // and add a request handler
183
-
184
- const stopCallbackList = createCallbackListNotifiedOnce()
185
- stopCallbackList.add(({ reason }) => {
186
- logger.info(`${serverName} stopping server (reason: ${reason})`)
187
- })
188
- stopCallbackList.add(async () => {
189
- await stopListening(nodeServer)
190
- })
191
- let stoppedResolve
192
- const stoppedPromise = new Promise((resolve) => {
193
- stoppedResolve = resolve
194
- })
195
- const stop = memoize(async (reason = STOP_REASON_NOT_SPECIFIED) => {
196
- status = "stopping"
197
- await Promise.all(stopCallbackList.notify({ reason }))
198
- serviceController.callHooks("serverStopped", { reason })
199
- status = "stopped"
200
- stoppedResolve(reason)
201
- })
202
-
203
- const cancelProcessTeardownRace = raceProcessTeardownEvents(
204
- processTeardownEvents,
205
- (winner) => {
206
- stop(PROCESS_TEARDOWN_EVENTS_MAP[winner.name])
207
- },
208
- )
209
- stopCallbackList.add(cancelProcessTeardownRace)
210
-
211
- const onError = (error) => {
212
- if (status === "stopping" && error.code === "ECONNRESET") {
213
- return
214
- }
215
- throw error
216
- }
217
-
218
- status = "opened"
219
- const serverOrigins = await getServerOrigins({ protocol, host, port })
220
- const serverOrigin = serverOrigins.local
221
-
222
- const removeConnectionErrorListener = listenServerConnectionError(
223
- nodeServer,
224
- onError,
225
- )
226
- stopCallbackList.add(removeConnectionErrorListener)
227
-
228
- const connectionsTracker = trackServerPendingConnections(nodeServer, {
229
- http2,
230
- })
231
- // opened connection must be shutdown before the close event is emitted
232
- stopCallbackList.add(connectionsTracker.stop)
233
-
234
- const pendingRequestsTracker = trackServerPendingRequests(nodeServer, {
235
- http2,
236
- })
237
- // ensure pending requests got a response from the server
238
- stopCallbackList.add((reason) => {
239
- pendingRequestsTracker.stop({
240
- status: reason === STOP_REASON_INTERNAL_ERROR ? 500 : 503,
241
- reason,
242
- })
243
- })
244
-
245
- const requestCallback = async (nodeRequest, nodeResponse) => {
246
- // pause the stream to let a chance to "requestToResponse"
247
- // to call "requestRequestBody". Without this the request body readable stream
248
- // might be closed when we'll try to attach "data" and "end" listeners to it
249
- nodeRequest.pause()
250
- if (!nagle) {
251
- nodeRequest.connection.setNoDelay(true)
252
- }
253
- if (redirectHttpToHttps && !nodeRequest.connection.encrypted) {
254
- nodeResponse.writeHead(301, {
255
- location: `${serverOrigin}${nodeRequest.url}`,
256
- })
257
- nodeResponse.end()
258
- return
259
- }
260
-
261
- const receiveRequestOperation = Abort.startOperation()
262
- receiveRequestOperation.addAbortSource((abort) => {
263
- const closeEventCallback = () => {
264
- if (nodeRequest.complete) {
265
- receiveRequestOperation.end()
266
- } else {
267
- nodeResponse.destroy()
268
- abort()
269
- }
270
- }
271
- nodeRequest.once("close", closeEventCallback)
272
- return () => {
273
- nodeRequest.removeListener("close", closeEventCallback)
274
- }
275
- })
276
- receiveRequestOperation.addAbortSource((abort) => {
277
- return stopCallbackList.add(abort)
278
- })
279
-
280
- const sendResponseOperation = Abort.startOperation()
281
- sendResponseOperation.addAbortSignal(receiveRequestOperation.signal)
282
- sendResponseOperation.addAbortSource((abort) => {
283
- return stopCallbackList.add(abort)
284
- })
285
-
286
- const request = fromNodeRequest(nodeRequest, {
287
- serverOrigin,
288
- signal: receiveRequestOperation.signal,
289
- })
290
-
291
- // Handling request is asynchronous, we buffer logs for that request
292
- // until we know what happens with that request
293
- // It delays logs until we know of the request will be handled
294
- // but it's mandatory to make logs readable.
295
- const rootRequestNode = {
296
- logs: [],
297
- children: [],
298
- }
299
- const addRequestLog = (node, { type, value }) => {
300
- node.logs.push({ type, value })
301
- }
302
- const onRequestHandled = (node) => {
303
- if (node !== rootRequestNode) {
304
- // keep buffering until root request write logs for everyone
305
- return
306
- }
307
- const prefixLines = (string, prefix) => {
308
- return string.replace(/^(?!\s*$)/gm, prefix)
309
- }
310
- const writeLog = (
311
- { type, value },
312
- { someLogIsError, someLogIsWarn, depth },
313
- ) => {
314
- if (depth > 0) {
315
- value = prefixLines(value, " ".repeat(depth))
316
- }
317
- if (type === "info") {
318
- if (someLogIsError) {
319
- type = "error"
320
- } else if (someLogIsWarn) {
321
- type = "warn"
322
- }
323
- }
324
- logger[type](value)
325
- }
326
- const visitRequestNodeToLog = (requestNode, depth) => {
327
- let someLogIsError = false
328
- let someLogIsWarn = false
329
- requestNode.logs.forEach((log) => {
330
- if (log.type === "error") {
331
- someLogIsError = true
332
- }
333
- if (log.type === "warn") {
334
- someLogIsWarn = true
335
- }
336
- })
337
-
338
- const firstLog = requestNode.logs.shift()
339
- const lastLog = requestNode.logs.pop()
340
- const middleLogs = requestNode.logs
341
-
342
- writeLog(firstLog, { someLogIsError, someLogIsWarn, depth })
343
- middleLogs.forEach((log) => {
344
- writeLog(log, { someLogIsError, someLogIsWarn, depth })
345
- })
346
- requestNode.children.forEach((child) => {
347
- visitRequestNodeToLog(child, depth + 1)
348
- })
349
- if (lastLog) {
350
- writeLog(lastLog, { someLogIsError, someLogIsWarn, depth: depth + 1 })
351
- }
352
- }
353
- visitRequestNodeToLog(rootRequestNode, 0)
354
- }
355
- nodeRequest.on("error", (error) => {
356
- if (error.message === "aborted") {
357
- addRequestLog(rootRequestNode, {
358
- type: "debug",
359
- value: createDetailedMessage(`request aborted by client`, {
360
- "error message": error.message,
361
- }),
362
- })
363
- } else {
364
- // I'm not sure this can happen but it's here in case
365
- addRequestLog(rootRequestNode, {
366
- type: "error",
367
- value: createDetailedMessage(`"error" event emitted on request`, {
368
- "error stack": error.stack,
369
- }),
370
- })
371
- }
372
- })
373
-
374
- const pushResponse = async ({ path, method }, { requestNode }) => {
375
- const http2Stream = nodeResponse.stream
376
-
377
- // being able to push a stream is nice to have
378
- // so when it fails it's not critical
379
- const onPushStreamError = (e) => {
380
- addRequestLog(requestNode, {
381
- type: "error",
382
- value: createDetailedMessage(
383
- `An error occured while pushing a stream to the response for ${request.ressource}`,
384
- {
385
- "error stack": e.stack,
386
- },
387
- ),
388
- })
389
- }
390
-
391
- // not aborted, let's try to push a stream into that response
392
- // https://nodejs.org/docs/latest-v16.x/api/http2.html#http2streampushstreamheaders-options-callback
393
- let pushStream
394
- try {
395
- pushStream = await new Promise((resolve, reject) => {
396
- http2Stream.pushStream(
397
- {
398
- ":path": path,
399
- ...(method ? { ":method": method } : {}),
400
- },
401
- async (
402
- error,
403
- pushStream,
404
- // headers
405
- ) => {
406
- if (error) {
407
- reject(error)
408
- }
409
- resolve(pushStream)
410
- },
411
- )
412
- })
413
- } catch (e) {
414
- onPushStreamError(e)
415
- return
416
- }
417
-
418
- const abortController = new AbortController()
419
- // It's possible to get NGHTTP2_REFUSED_STREAM errors here
420
- // https://github.com/nodejs/node/issues/20824
421
- const pushErrorCallback = (error) => {
422
- onPushStreamError(error)
423
- abortController.abort()
424
- }
425
- pushStream.on("error", pushErrorCallback)
426
- sendResponseOperation.addEndCallback(() => {
427
- pushStream.removeListener("error", onPushStreamError)
428
- })
429
-
430
- await sendResponseOperation.withSignal(async (signal) => {
431
- const pushResponseOperation = Abort.startOperation()
432
- pushResponseOperation.addAbortSignal(signal)
433
- pushResponseOperation.addAbortSignal(abortController.signal)
434
-
435
- const pushRequest = createPushRequest(request, {
436
- signal: pushResponseOperation.signal,
437
- pathname: path,
438
- method,
439
- })
440
-
441
- try {
442
- const responseProperties = await handleRequest(pushRequest, {
443
- requestNode,
444
- })
445
- if (!abortController.signal.aborted) {
446
- if (pushStream.destroyed) {
447
- abortController.abort()
448
- } else if (!http2Stream.pushAllowed) {
449
- abortController.abort()
450
- } else if (responseProperties !== ABORTED_RESPONSE_PROPERTIES) {
451
- const responseLength =
452
- responseProperties.headers["content-length"] || 0
453
- const { effectiveRecvDataLength, remoteWindowSize } =
454
- http2Stream.session.state
455
- if (effectiveRecvDataLength + responseLength > remoteWindowSize) {
456
- addRequestLog(requestNode, {
457
- type: "debug",
458
- value: `Aborting stream to prevent exceeding remoteWindowSize`,
459
- })
460
- abortController.abort()
461
- }
462
- }
463
- }
464
- await sendResponse({
465
- signal: pushResponseOperation.signal,
466
- request: pushRequest,
467
- requestNode,
468
- responseStream: pushStream,
469
- responseProperties,
470
- })
471
- } finally {
472
- await pushResponseOperation.end()
473
- }
474
- })
475
- }
476
-
477
- const handleRequest = async (request, { requestNode }) => {
478
- let requestReceivedMeasure
479
- if (serverTiming) {
480
- requestReceivedMeasure = performance.now()
481
- }
482
- addRequestLog(requestNode, {
483
- type: "info",
484
- value: request.parent
485
- ? `Push ${request.ressource}`
486
- : `${request.method} ${request.origin}${request.ressource}`,
487
- })
488
- const warn = (value) => {
489
- addRequestLog(requestNode, {
490
- type: "warn",
491
- value,
492
- })
493
- }
494
-
495
- let requestWaitingTimeout
496
- if (requestWaitingMs) {
497
- requestWaitingTimeout = setTimeout(
498
- () => requestWaitingCallback({ request, warn, requestWaitingMs }),
499
- requestWaitingMs,
500
- ).unref()
501
- }
502
-
503
- serviceController.callHooks(
504
- "redirectRequest",
505
- request,
506
- { warn },
507
- (newRequestProperties) => {
508
- if (newRequestProperties) {
509
- request = applyRedirectionToRequest(request, {
510
- original: request.original || request,
511
- previous: request,
512
- ...newRequestProperties,
513
- })
514
- }
515
- },
516
- )
517
-
518
- let handleRequestReturnValue
519
- let errorWhileHandlingRequest = null
520
- let handleRequestTimings = serverTiming ? {} : null
521
- try {
522
- handleRequestReturnValue = await serviceController.callAsyncHooksUntil(
523
- "handleRequest",
524
- request,
525
- {
526
- timing: handleRequestTimings,
527
- warn,
528
- pushResponse: async ({ path, method }) => {
529
- if (typeof path !== "string" || path[0] !== "/") {
530
- addRequestLog(requestNode, {
531
- type: "warn",
532
- value: `response push ignored because path is invalid (must be a string starting with "/", found ${path})`,
533
- })
534
- return
535
- }
536
- if (!request.http2) {
537
- addRequestLog(requestNode, {
538
- type: "warn",
539
- value: `response push ignored because request is not http2`,
540
- })
541
- return
542
- }
543
- const canPushStream = testCanPushStream(nodeResponse.stream)
544
- if (!canPushStream.can) {
545
- addRequestLog(requestNode, {
546
- type: "debug",
547
- value: `response push ignored because ${canPushStream.reason}`,
548
- })
549
- return
550
- }
551
-
552
- let preventedByService = null
553
- const prevent = () => {
554
- preventedByService = serviceController.getCurrentService()
555
- }
556
- serviceController.callHooksUntil(
557
- "onResponsePush",
558
- { path, method },
559
- {
560
- request,
561
- warn,
562
- prevent,
563
- },
564
- () => preventedByService,
565
- )
566
- if (preventedByService) {
567
- addRequestLog(requestNode, {
568
- type: "debug",
569
- value: `response push prevented by "${preventedByService.name}" service`,
570
- })
571
- return
572
- }
573
-
574
- const requestChildNode = { logs: [], children: [] }
575
- requestNode.children.push(requestChildNode)
576
- await pushResponse(
577
- { path, method },
578
- {
579
- requestNode: requestChildNode,
580
- parentHttp2Stream: nodeResponse.stream,
581
- },
582
- )
583
- },
584
- },
585
- )
586
- } catch (error) {
587
- errorWhileHandlingRequest = error
588
- }
589
-
590
- let responseProperties
591
- if (errorWhileHandlingRequest) {
592
- if (
593
- errorWhileHandlingRequest.name === "AbortError" &&
594
- request.signal.aborted
595
- ) {
596
- responseProperties = ABORTED_RESPONSE_PROPERTIES
597
- } else {
598
- // internal error, create 500 response
599
- if (
600
- // stopOnInternalError stops server only if requestToResponse generated
601
- // a non controlled error (internal error).
602
- // if requestToResponse gracefully produced a 500 response (it did not throw)
603
- // then we can assume we are still in control of what we are doing
604
- stopOnInternalError
605
- ) {
606
- // il faudrais pouvoir stop que les autres response ?
607
- stop(STOP_REASON_INTERNAL_ERROR)
608
- }
609
- const handleErrorReturnValue =
610
- await serviceController.callAsyncHooksUntil(
611
- "handleError",
612
- errorWhileHandlingRequest,
613
- {
614
- request,
615
- warn,
616
- },
617
- )
618
- if (!handleErrorReturnValue) {
619
- throw errorWhileHandlingRequest
620
- }
621
- addRequestLog(requestNode, {
622
- type: "error",
623
- value: createDetailedMessage(
624
- `internal error while handling request`,
625
- {
626
- "error stack": errorWhileHandlingRequest.stack,
627
- },
628
- ),
629
- })
630
- responseProperties = composeTwoResponses(
631
- {
632
- status: 500,
633
- statusText: "Internal Server Error",
634
- headers: {
635
- // ensure error are not cached
636
- "cache-control": "no-store",
637
- "content-type": "text/plain",
638
- },
639
- },
640
- handleErrorReturnValue,
641
- )
642
- }
643
- } else {
644
- const {
645
- status = 501,
646
- statusText,
647
- statusMessage,
648
- headers = {},
649
- body,
650
- ...rest
651
- } = handleRequestReturnValue || {}
652
- responseProperties = {
653
- status,
654
- statusText,
655
- statusMessage,
656
- headers,
657
- body,
658
- ...rest,
659
- }
660
- }
661
-
662
- if (serverTiming) {
663
- const responseReadyMeasure = performance.now()
664
- const timeToStartResponding =
665
- responseReadyMeasure - requestReceivedMeasure
666
- const serverTiming = {
667
- ...handleRequestTimings,
668
- ...responseProperties.timing,
669
- "time to start responding": timeToStartResponding,
670
- }
671
- responseProperties.headers = composeTwoHeaders(
672
- responseProperties.headers,
673
- timingToServerTimingResponseHeaders(serverTiming),
674
- )
675
- }
676
- if (requestWaitingMs) {
677
- clearTimeout(requestWaitingTimeout)
678
- }
679
- if (
680
- request.method !== "HEAD" &&
681
- responseProperties.headers["content-length"] > 0 &&
682
- !responseProperties.body
683
- ) {
684
- addRequestLog(requestNode, {
685
- type: "warn",
686
- value: `content-length header is ${responseProperties.headers["content-length"]} but body is empty`,
687
- })
688
- }
689
- serviceController.callHooks(
690
- "injectResponseHeaders",
691
- responseProperties,
692
- {
693
- request,
694
- warn,
695
- },
696
- (returnValue) => {
697
- if (returnValue) {
698
- responseProperties.headers = composeTwoHeaders(
699
- responseProperties.headers,
700
- returnValue,
701
- )
702
- }
703
- },
704
- )
705
- serviceController.callHooks("responseReady", responseProperties, {
706
- request,
707
- warn,
708
- })
709
- return responseProperties
710
- }
711
-
712
- const sendResponse = async ({
713
- signal,
714
- request,
715
- requestNode,
716
- responseStream,
717
- responseProperties,
718
- }) => {
719
- // When "pushResponse" is called and the parent response has no body
720
- // the parent response is immediatly ended. It means child responses (pushed streams)
721
- // won't get a chance to be pushed.
722
- // To let a chance to pushed streams we wait a little before sending the response
723
- const ignoreBody = request.method === "HEAD"
724
- const bodyIsEmpty = !responseProperties.body || ignoreBody
725
- if (bodyIsEmpty && requestNode.children.length > 0) {
726
- await new Promise((resolve) => setTimeout(resolve))
727
- }
728
-
729
- await populateNodeResponse(responseStream, responseProperties, {
730
- signal,
731
- ignoreBody,
732
- onAbort: () => {
733
- addRequestLog(requestNode, {
734
- type: "info",
735
- value: `response aborted`,
736
- })
737
- onRequestHandled(requestNode)
738
- },
739
- onError: (error) => {
740
- addRequestLog(requestNode, {
741
- type: "error",
742
- value: createDetailedMessage(
743
- `An error occured while sending response`,
744
- {
745
- "error stack": error.stack,
746
- },
747
- ),
748
- })
749
- onRequestHandled(requestNode)
750
- },
751
- onHeadersSent: ({ status, statusText }) => {
752
- const statusType = statusToType(status)
753
- addRequestLog(requestNode, {
754
- type: {
755
- information: "info",
756
- success: "info",
757
- redirection: "info",
758
- client_error: "warn",
759
- server_error: "error",
760
- }[statusType],
761
- value: `${colorizeResponseStatus(status)} ${
762
- responseProperties.statusMessage || statusText
763
- }`,
764
- })
765
- },
766
- onEnd: () => {
767
- onRequestHandled(requestNode)
768
- },
769
- })
770
- }
771
-
772
- try {
773
- if (receiveRequestOperation.signal.aborted) {
774
- return
775
- }
776
- const responseProperties = await handleRequest(request, {
777
- requestNode: rootRequestNode,
778
- })
779
- nodeRequest.resume()
780
- if (receiveRequestOperation.signal.aborted) {
781
- return
782
- }
783
-
784
- // the node request readable stream is never closed because
785
- // the response headers contains "connection: keep-alive"
786
- // In this scenario we want to disable READABLE_STREAM_TIMEOUT warning
787
- if (responseProperties.headers.connection === "keep-alive") {
788
- clearTimeout(request.body.timeout)
789
- }
790
-
791
- await sendResponse({
792
- signal: sendResponseOperation.signal,
793
- request,
794
- requestNode: rootRequestNode,
795
- responseStream: nodeResponse,
796
- responseProperties,
797
- })
798
- } finally {
799
- await sendResponseOperation.end()
800
- }
801
- }
802
-
803
- const removeRequestListener = listenRequest(nodeServer, requestCallback)
804
- // ensure we don't try to handle new requests while server is stopping
805
- stopCallbackList.add(removeRequestListener)
806
-
807
- if (startLog) {
808
- if (serverOrigins.network) {
809
- logger.info(
810
- `${serverName} started at ${serverOrigins.local} (${serverOrigins.network})`,
811
- )
812
- } else {
813
- logger.info(`${serverName} started at ${serverOrigins.local}`)
814
- }
815
- }
816
-
817
- const addEffect = (callback) => {
818
- const cleanup = callback()
819
- if (typeof cleanup === "function") {
820
- stopCallbackList.add(cleanup)
821
- }
822
- }
823
-
824
- return {
825
- getStatus: () => status,
826
- port,
827
- origin: serverOrigin,
828
- origins: serverOrigins,
829
- nodeServer,
830
- stop,
831
- stoppedPromise,
832
- addEffect,
833
- }
834
- }
835
-
836
- const createNodeServer = async ({
837
- protocol,
838
- redirectHttpToHttps,
839
- allowHttpRequestOnHttps,
840
- certificate,
841
- privateKey,
842
- http2,
843
- http1Allowed,
844
- }) => {
845
- if (protocol === "http") {
846
- return http.createServer()
847
- }
848
-
849
- if (redirectHttpToHttps || allowHttpRequestOnHttps) {
850
- return createPolyglotServer({
851
- certificate,
852
- privateKey,
853
- http2,
854
- http1Allowed,
855
- })
856
- }
857
-
858
- const { createServer } = await import("node:https")
859
- return createServer({
860
- cert: certificate,
861
- key: privateKey,
862
- })
863
- }
864
-
865
- const testCanPushStream = (http2Stream) => {
866
- if (!http2Stream.pushAllowed) {
867
- return {
868
- can: false,
869
- reason: `stream.pushAllowed is false`,
870
- }
871
- }
872
-
873
- // See https://nodejs.org/dist/latest-v16.x/docs/api/http2.html#http2sessionstate
874
- // And https://github.com/google/node-h2-auto-push/blob/67a36c04cbbd6da7b066a4e8d361c593d38853a4/src/index.ts#L100-L106
875
- const { remoteWindowSize } = http2Stream.session.state
876
- if (remoteWindowSize === 0) {
877
- return {
878
- can: false,
879
- reason: `no more remoteWindowSize`,
880
- }
881
- }
882
-
883
- return {
884
- can: true,
885
- }
886
- }
887
-
888
- const ABORTED_RESPONSE_PROPERTIES = {}
889
-
890
- const PROCESS_TEARDOWN_EVENTS_MAP = {
891
- SIGHUP: STOP_REASON_PROCESS_SIGHUP,
892
- SIGTERM: STOP_REASON_PROCESS_SIGTERM,
893
- SIGINT: STOP_REASON_PROCESS_SIGINT,
894
- beforeExit: STOP_REASON_PROCESS_BEFORE_EXIT,
895
- exit: STOP_REASON_PROCESS_EXIT,
896
- }