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