@jsenv/server 15.5.2 → 16.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 (55) hide show
  1. package/README.md +79 -86
  2. package/dist/html/4xx.html +196 -0
  3. package/dist/html/500.html +17 -0
  4. package/dist/html/route_inspector.html +2435 -0
  5. package/dist/js/formidable.js +17 -0
  6. package/dist/js/vendors.js +2001 -0
  7. package/dist/js/ws.js +6863 -0
  8. package/dist/jsenv_server.js +8829 -0
  9. package/package.json +20 -9
  10. package/src/content_negotiation/pick_content_version.js +63 -0
  11. package/src/{fetch_filesystem.js → filesystem/fetch_filesystem.js} +57 -72
  12. package/src/{serve_directory.js → filesystem/serve_directory.js} +1 -2
  13. package/src/interfacing_with_node/from_node_request.js +455 -0
  14. package/src/interfacing_with_node/get_observable_value_type.js +43 -0
  15. package/src/interfacing_with_node/observable.js +91 -75
  16. package/src/interfacing_with_node/observable_from.js +34 -0
  17. package/src/interfacing_with_node/observable_from_file_handle.js +30 -0
  18. package/src/interfacing_with_node/observable_from_node_stream.js +44 -46
  19. package/src/interfacing_with_node/observable_from_node_web_readable_stream.js +32 -0
  20. package/src/interfacing_with_node/observable_from_promise.js +13 -0
  21. package/src/interfacing_with_node/write_node_response.js +80 -35
  22. package/src/internal/colorizeResponseStatus.js +2 -0
  23. package/src/internal/headers_composition.js +42 -15
  24. package/src/internal/multiple-header.js +27 -12
  25. package/src/internal/object_composition.js +2 -2
  26. package/src/internal/response_composition.js +16 -3
  27. package/src/main.js +15 -11
  28. package/src/progressive_response.js +38 -0
  29. package/src/replace_placeholder_in_html.js +12 -0
  30. package/src/request_body_handling.js +22 -0
  31. package/src/router/router.js +878 -0
  32. package/src/server_events.js +431 -0
  33. package/src/service_controller.js +79 -73
  34. package/src/services/autoreload_on_server_restart/client/autoreload_on_server_restart.js +125 -0
  35. package/src/services/autoreload_on_server_restart/jsenv_service_autoreload_on_server_restart.js +45 -0
  36. package/src/services/cors/jsenv_service_cors.js +10 -18
  37. package/src/services/default_body_4xx_5xx/client/4xx.html +65 -0
  38. package/src/services/default_body_4xx_5xx/jsenv_service_default_body_4xx_5xx.js +82 -0
  39. package/src/services/error_handler/client/500.html +17 -0
  40. package/src/services/error_handler/jsenv_service_error_handler.js +47 -28
  41. package/src/services/internal_client_files/jsenv_service_internal_client_files.js +34 -0
  42. package/src/services/open_file/jsenv_service_open_file.js +37 -0
  43. package/src/services/route_inspector/client/code_block.js +215 -0
  44. package/src/services/route_inspector/client/route_inspector.html +1363 -0
  45. package/src/services/route_inspector/jsenv_service_route_inspector.js +39 -0
  46. package/src/start_server.js +575 -607
  47. package/src/web_socket_response.js +53 -0
  48. package/src/from_fetch_response.js +0 -15
  49. package/src/interfacing_with_node/body.js +0 -81
  50. package/src/interfacing_with_node/request_factory.js +0 -125
  51. package/src/readRequestBody.js +0 -32
  52. package/src/server_timing/timing_measure.js +0 -26
  53. package/src/service_composition/path_to_regexp.js +0 -436
  54. package/src/service_composition/routing.js +0 -31
  55. package/src/sse/sse_room.js +0 -313
@@ -8,13 +8,13 @@ import {
8
8
  applyRedirectionToRequest,
9
9
  createPushRequest,
10
10
  fromNodeRequest,
11
- } from "./interfacing_with_node/request_factory.js";
11
+ } from "./interfacing_with_node/from_node_request.js";
12
12
  import { writeNodeResponse } from "./interfacing_with_node/write_node_response.js";
13
+ import { websocketSuffixColorized } from "./internal/colorizeResponseStatus.js";
13
14
  import {
14
- colorizeResponseStatus,
15
- statusToType,
16
- } from "./internal/colorizeResponseStatus.js";
17
- import { composeTwoHeaders } from "./internal/headers_composition.js";
15
+ composeTwoHeaders,
16
+ composeTwoHeaderValues,
17
+ } from "./internal/headers_composition.js";
18
18
  import { listen, stopListening } from "./internal/listen.js";
19
19
  import { listenEvent } from "./internal/listenEvent.js";
20
20
  import { listenRequest } from "./internal/listenRequest.js";
@@ -23,8 +23,17 @@ import { composeTwoResponses } from "./internal/response_composition.js";
23
23
  import { createPolyglotServer } from "./internal/server-polyglot.js";
24
24
  import { trackServerPendingConnections } from "./internal/trackServerPendingConnections.js";
25
25
  import { trackServerPendingRequests } from "./internal/trackServerPendingRequests.js";
26
+ import { createRouter } from "./router/router.js";
26
27
  import { timingToServerTimingResponseHeaders } from "./server_timing/timing_header.js";
27
- import { createServiceController } from "./service_controller.js";
28
+ import {
29
+ createServiceController,
30
+ flattenAndFilterServices,
31
+ } from "./service_controller.js";
32
+ import { jsenvServiceAutoreloadOnRestart } from "./services/autoreload_on_server_restart/jsenv_service_autoreload_on_server_restart.js";
33
+ import { jsenvServiceDefaultBody4xx5xx } from "./services/default_body_4xx_5xx/jsenv_service_default_body_4xx_5xx.js";
34
+ import { jsenvServiceInternalClientFiles } from "./services/internal_client_files/jsenv_service_internal_client_files.js";
35
+ import { jsenvServiceOpenFile } from "./services/open_file/jsenv_service_open_file.js";
36
+ import { jsenvServiceRouteInspector } from "./services/route_inspector/jsenv_service_route_inspector.js";
28
37
  import {
29
38
  STOP_REASON_INTERNAL_ERROR,
30
39
  STOP_REASON_NOT_SPECIFIED,
@@ -34,11 +43,16 @@ import {
34
43
  STOP_REASON_PROCESS_SIGINT,
35
44
  STOP_REASON_PROCESS_SIGTERM,
36
45
  } from "./stopReasons.js";
46
+ import { getWebSocketHandler } from "./web_socket_response.js";
37
47
 
38
48
  import { applyDnsResolution } from "./internal/dns_resolution.js";
39
49
  import { parseHostname } from "./internal/hostname_parser.js";
40
50
  import { createIpGetters } from "./internal/server_ips.js";
41
51
 
52
+ const TIMING_NOOP = () => {
53
+ return { end: () => {} };
54
+ };
55
+
42
56
  export const startServer = async ({
43
57
  signal = new AbortController().signal,
44
58
  logLevel,
@@ -65,12 +79,13 @@ export const startServer = async ({
65
79
  // auto close when requestToResponse throw an error
66
80
  stopOnInternalError = false,
67
81
  keepProcessAlive = true,
82
+ routes = [],
68
83
  services = [],
69
84
  nagle = true,
70
85
  serverTiming = false,
71
86
  requestWaitingMs = 0,
72
- requestWaitingCallback = ({ request, warn, requestWaitingMs }) => {
73
- warn(
87
+ requestWaitingCallback = ({ request, requestWaitingMs }) => {
88
+ request.logger.warn(
74
89
  createDetailedMessage(
75
90
  `still no response found for request after ${requestWaitingMs} ms`,
76
91
  {
@@ -139,7 +154,35 @@ export const startServer = async ({
139
154
  }
140
155
  }
141
156
 
157
+ services = [
158
+ jsenvServiceOpenFile(),
159
+ jsenvServiceDefaultBody4xx5xx(),
160
+ jsenvServiceRouteInspector(),
161
+ jsenvServiceInternalClientFiles(),
162
+ jsenvServiceAutoreloadOnRestart(),
163
+ ...flattenAndFilterServices(services),
164
+ ];
165
+
166
+ const allRouteArray = [];
167
+ for (const route of routes) {
168
+ allRouteArray.push(route);
169
+ }
170
+ for (const service of services) {
171
+ const serviceRoutes = service.routes;
172
+ if (serviceRoutes) {
173
+ for (const serviceRoute of serviceRoutes) {
174
+ serviceRoute.service = service;
175
+ allRouteArray.push(serviceRoute);
176
+ }
177
+ }
178
+ }
179
+
180
+ const router = createRouter(allRouteArray, {
181
+ optionsFallback: true,
182
+ });
183
+
142
184
  const server = {};
185
+
143
186
  const serviceController = createServiceController(services);
144
187
  const processTeardownEvents = {
145
188
  SIGHUP: stopOnExit,
@@ -277,7 +320,9 @@ export const startServer = async ({
277
320
  // we can proceed to create a stop function to stop it gacefully
278
321
  // and add a request handler
279
322
  stopCallbackSet.add(({ reason }) => {
280
- logger.info(`${serverName} stopping server (reason: ${reason})`);
323
+ if (reason !== STOP_REASON_PROCESS_BEFORE_EXIT) {
324
+ logger.info(`${serverName} stopping server (reason: ${reason})`);
325
+ }
281
326
  });
282
327
  stopCallbackSet.add(async () => {
283
328
  await stopListening(nodeServer);
@@ -348,15 +393,279 @@ export const startServer = async ({
348
393
  });
349
394
  });
350
395
 
351
- request: {
352
- const requestCallback = async (nodeRequest, nodeResponse) => {
353
- // pause the stream to let a chance to "requestToResponse"
354
- // to call "requestRequestBody". Without this the request body readable stream
355
- // might be closed when we'll try to attach "data" and "end" listeners to it
356
- nodeRequest.pause();
357
- if (!nagle) {
358
- nodeRequest.connection.setNoDelay(true);
396
+ const applyRequestInternalRedirection = (request) => {
397
+ serviceController.callHooks(
398
+ "redirectRequest",
399
+ request,
400
+ {},
401
+ (newRequestProperties) => {
402
+ if (newRequestProperties) {
403
+ request = applyRedirectionToRequest(request, {
404
+ original: request.original || request,
405
+ previous: request,
406
+ ...newRequestProperties,
407
+ });
408
+ }
409
+ },
410
+ );
411
+ return request;
412
+ };
413
+
414
+ const prepareHandleRequestOperations = (nodeRequest, nodeResponse) => {
415
+ const receiveRequestOperation = Abort.startOperation();
416
+ receiveRequestOperation.addAbortSignal(stopAbortSignal);
417
+ const sendResponseOperation = Abort.startOperation();
418
+ sendResponseOperation.addAbortSignal(stopAbortSignal);
419
+ receiveRequestOperation.addAbortSource((abort) => {
420
+ const closeEventCallback = () => {
421
+ if (nodeRequest.complete) {
422
+ receiveRequestOperation.end();
423
+ } else {
424
+ nodeResponse.destroy();
425
+ abort();
426
+ }
427
+ };
428
+ nodeRequest.once("close", closeEventCallback);
429
+ return () => {
430
+ nodeRequest.removeListener("close", closeEventCallback);
431
+ };
432
+ });
433
+ sendResponseOperation.addAbortSignal(receiveRequestOperation.signal);
434
+ return [receiveRequestOperation, sendResponseOperation];
435
+ };
436
+ const getResponseProperties = async (request, { pushResponse }) => {
437
+ const timings = {};
438
+ const timing = serverTiming
439
+ ? (name) => {
440
+ const start = performance.now();
441
+ timings[name] = null;
442
+ return {
443
+ name,
444
+ end: () => {
445
+ const end = performance.now();
446
+ const duration = end - start;
447
+ timings[name] = duration;
448
+ },
449
+ };
450
+ }
451
+ : TIMING_NOOP;
452
+ const startRespondingTiming = timing("time to start responding");
453
+
454
+ request.logger.info(
455
+ request.headers["upgrade"] === "websocket"
456
+ ? `GET ${request.url} ${websocketSuffixColorized}`
457
+ : request.parent
458
+ ? `Push ${request.resource}`
459
+ : `${request.method} ${request.url}`,
460
+ );
461
+ let requestWaitingTimeout;
462
+ if (requestWaitingMs) {
463
+ requestWaitingTimeout = setTimeout(
464
+ () =>
465
+ requestWaitingCallback({
466
+ request,
467
+ requestWaitingMs,
468
+ }),
469
+ requestWaitingMs,
470
+ ).unref();
471
+ }
472
+
473
+ let headersToInject;
474
+ const finalizeResponseProperties = (responseProperties) => {
475
+ if (serverTiming) {
476
+ startRespondingTiming.end();
477
+ responseProperties.headers = composeTwoHeaders(
478
+ responseProperties.headers,
479
+ timingToServerTimingResponseHeaders(timings),
480
+ );
481
+ }
482
+ if (requestWaitingMs) {
483
+ clearTimeout(requestWaitingTimeout);
484
+ }
485
+ if (
486
+ request.method !== "HEAD" &&
487
+ responseProperties.headers &&
488
+ responseProperties.headers["content-length"] > 0 &&
489
+ !responseProperties.body
490
+ ) {
491
+ request.logger.warn(
492
+ `content-length response header found without body`,
493
+ );
494
+ }
495
+
496
+ if (headersToInject) {
497
+ responseProperties.headers = composeTwoHeaders(
498
+ responseProperties.headers,
499
+ headersToInject,
500
+ );
501
+ }
502
+ serviceController.callHooks(
503
+ "injectResponseProperties",
504
+ request,
505
+ responseProperties,
506
+ (returnValue) => {
507
+ if (!returnValue) {
508
+ return;
509
+ }
510
+ responseProperties = composeTwoResponses(
511
+ responseProperties,
512
+ returnValue,
513
+ );
514
+ },
515
+ );
516
+ // the node request readable stream is never closed because
517
+ // the response headers contains "connection: keep-alive"
518
+ // In this scenario we want to disable READABLE_STREAM_TIMEOUT warning
519
+ if (
520
+ responseProperties.headers.connection === "keep-alive" &&
521
+ request.body
522
+ ) {
523
+ clearTimeout(request.body.timeout);
524
+ }
525
+ return responseProperties;
526
+ };
527
+
528
+ let timeout;
529
+ try {
530
+ request = applyRequestInternalRedirection(request);
531
+ const timeoutResponsePropertiesPromise = new Promise((resolve) => {
532
+ timeout = setTimeout(() => {
533
+ resolve({
534
+ // the correct status code should be 500 because it's
535
+ // we don't really know what takes time
536
+ // in practice it's often because server is trying to reach an other server
537
+ // that is not responding so 504 is more correct
538
+ status: 504,
539
+ statusText: `server timeout after ${
540
+ responseTimeout / 1000
541
+ }s waiting to handle request`,
542
+ });
543
+ }, responseTimeout);
544
+ });
545
+ const routerResponsePropertiesPromise = (async () => {
546
+ const fetchSecondArg = {
547
+ timing,
548
+ pushResponse,
549
+ injectResponseHeader: (name, value) => {
550
+ if (!headersToInject) {
551
+ headersToInject = {};
552
+ }
553
+ headersToInject[name] = composeTwoHeaderValues(
554
+ name,
555
+ headersToInject[name],
556
+ value,
557
+ );
558
+ },
559
+ };
560
+ serviceController.callHooks(
561
+ "augmentRouteFetchSecondArg",
562
+ request,
563
+ fetchSecondArg,
564
+ (properties) => {
565
+ if (properties) {
566
+ Object.assign(fetchSecondArg, properties);
567
+ }
568
+ },
569
+ );
570
+ const routerResponseProperties = await router.match(
571
+ request,
572
+ fetchSecondArg,
573
+ );
574
+ return routerResponseProperties;
575
+ })();
576
+ const responseProperties = await Promise.race([
577
+ timeoutResponsePropertiesPromise,
578
+ routerResponsePropertiesPromise,
579
+ ]);
580
+ clearTimeout(timeout);
581
+ return finalizeResponseProperties(responseProperties);
582
+ } catch (e) {
583
+ clearTimeout(timeout);
584
+ if (e.name === "AbortError" && request.signal.aborted) {
585
+ // let it propagate to the caller that should catch this
586
+ throw e;
587
+ }
588
+ // internal error, create 500 response
589
+ if (
590
+ // stopOnInternalError stops server only if requestToResponse generated
591
+ // a non controlled error (internal error).
592
+ // if requestToResponse gracefully produced a 500 response (it did not throw)
593
+ // then we can assume we are still in control of what we are doing
594
+ stopOnInternalError
595
+ ) {
596
+ // il faudrais pouvoir stop que les autres response ?
597
+ stop(STOP_REASON_INTERNAL_ERROR);
359
598
  }
599
+ const handleErrorReturnValue =
600
+ await serviceController.callAsyncHooksUntil("handleError", e, {
601
+ request,
602
+ });
603
+ if (!handleErrorReturnValue) {
604
+ throw e;
605
+ }
606
+ request.logger.error(
607
+ createDetailedMessage(`internal error while handling request`, {
608
+ "error stack": e.stack,
609
+ }),
610
+ );
611
+ const responseProperties = composeTwoResponses(
612
+ {
613
+ status: 500,
614
+ statusText: "Internal Server Error",
615
+ headers: {
616
+ // ensure error are not cached
617
+ "cache-control": "no-store",
618
+ },
619
+ },
620
+ handleErrorReturnValue,
621
+ );
622
+ return finalizeResponseProperties(responseProperties);
623
+ }
624
+ };
625
+ const sendResponse = async (
626
+ responseStream,
627
+ responseProperties,
628
+ { signal, request },
629
+ ) => {
630
+ // When "pushResponse" is called and the parent response has no body
631
+ // the parent response is immediatly ended. It means child responses (pushed streams)
632
+ // won't get a chance to be pushed.
633
+ // To let a chance to pushed streams we wait a little before sending the response
634
+ const ignoreBody = request.method === "HEAD";
635
+ const bodyIsEmpty = !responseProperties.body || ignoreBody;
636
+ if (bodyIsEmpty && request.logger.hasPushChild) {
637
+ await new Promise((resolve) => setTimeout(resolve));
638
+ }
639
+ await writeNodeResponse(responseStream, responseProperties, {
640
+ signal,
641
+ ignoreBody,
642
+ onAbort: () => {
643
+ request.logger.info(`response aborted`);
644
+ request.logger.end();
645
+ },
646
+ onError: (error) => {
647
+ request.logger.error(
648
+ createDetailedMessage(`An error occured while sending response`, {
649
+ "error stack": error.stack,
650
+ }),
651
+ );
652
+ request.logger.end();
653
+ },
654
+ onHeadersSent: ({ status, statusText }) => {
655
+ request.logger.onHeadersSent({
656
+ status,
657
+ statusText: responseProperties.statusMessage || statusText,
658
+ });
659
+ request.logger.end();
660
+ },
661
+ onEnd: () => {
662
+ request.logger.end();
663
+ },
664
+ });
665
+ };
666
+
667
+ request: {
668
+ const requestEventHandler = async (nodeRequest, nodeResponse) => {
360
669
  if (redirectHttpToHttps && !nodeRequest.connection.encrypted) {
361
670
  nodeResponse.writeHead(301, {
362
671
  location: `${serverOrigin}${nodeRequest.url}`,
@@ -373,642 +682,301 @@ export const startServer = async ({
373
682
  return;
374
683
  }
375
684
 
376
- const receiveRequestOperation = Abort.startOperation();
377
- receiveRequestOperation.addAbortSignal(stopAbortSignal);
378
- const sendResponseOperation = Abort.startOperation();
379
- sendResponseOperation.addAbortSignal(stopAbortSignal);
380
- receiveRequestOperation.addAbortSource((abort) => {
381
- const closeEventCallback = () => {
382
- if (nodeRequest.complete) {
383
- receiveRequestOperation.end();
384
- } else {
385
- nodeResponse.destroy();
386
- abort();
387
- }
388
- };
389
- nodeRequest.once("close", closeEventCallback);
390
- return () => {
391
- nodeRequest.removeListener("close", closeEventCallback);
392
- };
393
- });
394
- sendResponseOperation.addAbortSignal(receiveRequestOperation.signal);
395
-
685
+ const [receiveRequestOperation, sendResponseOperation] =
686
+ prepareHandleRequestOperations(nodeRequest, nodeResponse);
396
687
  const request = fromNodeRequest(nodeRequest, {
397
688
  signal: stopAbortSignal,
398
689
  serverOrigin,
690
+ requestBodyLifetime,
691
+ logger,
692
+ nagle,
399
693
  });
400
694
 
401
- // Handling request is asynchronous, we buffer logs for that request
402
- // until we know what happens with that request
403
- // It delays logs until we know of the request will be handled
404
- // but it's mandatory to make logs readable.
405
- const rootRequestNode = {
406
- logs: [],
407
- children: [],
408
- };
409
- const addRequestLog = (node, { type, value }) => {
410
- node.logs.push({ type, value });
411
- };
412
- const onRequestHandled = (node) => {
413
- if (node !== rootRequestNode) {
414
- // keep buffering until root request write logs for everyone
415
- return;
416
- }
417
- const prefixLines = (string, prefix) => {
418
- return string.replace(/^(?!\s*$)/gm, prefix);
419
- };
420
- const writeLog = (
421
- { type, value },
422
- { someLogIsError, someLogIsWarn, depth },
423
- ) => {
424
- if (depth > 0) {
425
- value = prefixLines(value, " ".repeat(depth));
426
- }
427
- if (type === "info") {
428
- if (someLogIsError) {
429
- type = "error";
430
- } else if (someLogIsWarn) {
431
- type = "warn";
695
+ try {
696
+ const responseProperties = await getResponseProperties(request, {
697
+ pushResponse: async ({ path, method }) => {
698
+ const pushRequestLogger = request.logger.forPush();
699
+ if (typeof path !== "string" || path[0] !== "/") {
700
+ pushRequestLogger.warn(
701
+ `response push ignored because path is invalid (must be a string starting with "/", found ${path})`,
702
+ );
703
+ return;
432
704
  }
433
- }
434
- logger[type](value);
435
- };
436
- const visitRequestNodeToLog = (requestNode, depth) => {
437
- let someLogIsError = false;
438
- let someLogIsWarn = false;
439
- requestNode.logs.forEach((log) => {
440
- if (log.type === "error") {
441
- someLogIsError = true;
705
+ if (!request.http2) {
706
+ pushRequestLogger.warn(
707
+ `response push ignored because request is not http2`,
708
+ );
709
+ return;
442
710
  }
443
- if (log.type === "warn") {
444
- someLogIsWarn = true;
711
+ const canPushStream = testCanPushStream(nodeResponse.stream);
712
+ if (!canPushStream.can) {
713
+ pushRequestLogger.debug(
714
+ `response push ignored because ${canPushStream.reason}`,
715
+ );
716
+ return;
445
717
  }
446
- });
447
-
448
- const firstLog = requestNode.logs.shift();
449
- const lastLog = requestNode.logs.pop();
450
- const middleLogs = requestNode.logs;
451
-
452
- writeLog(firstLog, { someLogIsError, someLogIsWarn, depth });
453
- middleLogs.forEach((log) => {
454
- writeLog(log, { someLogIsError, someLogIsWarn, depth });
455
- });
456
- requestNode.children.forEach((child) => {
457
- visitRequestNodeToLog(child, depth + 1);
458
- });
459
- if (lastLog) {
460
- writeLog(lastLog, {
461
- someLogIsError,
462
- someLogIsWarn,
463
- depth: depth + 1,
464
- });
465
- }
466
- };
467
- visitRequestNodeToLog(rootRequestNode, 0);
468
- };
469
- nodeRequest.on("error", (error) => {
470
- if (error.message === "aborted") {
471
- addRequestLog(rootRequestNode, {
472
- type: "debug",
473
- value: createDetailedMessage(`request aborted by client`, {
474
- "error message": error.message,
475
- }),
476
- });
477
- } else {
478
- // I'm not sure this can happen but it's here in case
479
- addRequestLog(rootRequestNode, {
480
- type: "error",
481
- value: createDetailedMessage(`"error" event emitted on request`, {
482
- "error stack": error.stack,
483
- }),
484
- });
485
- }
486
- });
487
-
488
- const pushResponse = async ({ path, method }, { requestNode }) => {
489
- const http2Stream = nodeResponse.stream;
490
718
 
491
- // being able to push a stream is nice to have
492
- // so when it fails it's not critical
493
- const onPushStreamError = (e) => {
494
- addRequestLog(requestNode, {
495
- type: "error",
496
- value: createDetailedMessage(
497
- `An error occured while pushing a stream to the response for ${request.resource}`,
498
- {
499
- "error stack": e.stack,
500
- },
501
- ),
502
- });
503
- };
504
-
505
- // not aborted, let's try to push a stream into that response
506
- // https://nodejs.org/docs/latest-v16.x/api/http2.html#http2streampushstreamheaders-options-callback
507
- let pushStream;
508
- try {
509
- pushStream = await new Promise((resolve, reject) => {
510
- http2Stream.pushStream(
511
- {
512
- ":path": path,
513
- ...(method ? { ":method": method } : {}),
514
- },
515
- async (
516
- error,
517
- pushStream,
518
- // headers
519
- ) => {
520
- if (error) {
521
- reject(error);
522
- }
523
- resolve(pushStream);
524
- },
719
+ let preventedByService = null;
720
+ const prevent = () => {
721
+ preventedByService = serviceController.getCurrentService();
722
+ };
723
+ serviceController.callHooksUntil(
724
+ "onResponsePush",
725
+ { path, method },
726
+ { request, prevent },
727
+ () => preventedByService,
525
728
  );
526
- });
527
- } catch (e) {
528
- onPushStreamError(e);
529
- return;
530
- }
531
-
532
- const abortController = new AbortController();
533
- // It's possible to get NGHTTP2_REFUSED_STREAM errors here
534
- // https://github.com/nodejs/node/issues/20824
535
- const pushErrorCallback = (error) => {
536
- onPushStreamError(error);
537
- abortController.abort();
538
- };
539
- pushStream.on("error", pushErrorCallback);
540
- sendResponseOperation.addEndCallback(() => {
541
- pushStream.removeListener("error", onPushStreamError);
542
- });
543
-
544
- await sendResponseOperation.withSignal(async (signal) => {
545
- const pushResponseOperation = Abort.startOperation();
546
- pushResponseOperation.addAbortSignal(signal);
547
- pushResponseOperation.addAbortSignal(abortController.signal);
548
-
549
- const pushRequest = createPushRequest(request, {
550
- signal: pushResponseOperation.signal,
551
- pathname: path,
552
- method,
553
- });
554
-
555
- try {
556
- const responseProperties = await handleRequest(pushRequest, {
557
- requestNode,
558
- });
559
- if (!abortController.signal.aborted) {
560
- if (pushStream.destroyed) {
561
- abortController.abort();
562
- } else if (!http2Stream.pushAllowed) {
563
- abortController.abort();
564
- } else if (responseProperties.requestAborted) {
565
- } else {
566
- const responseLength =
567
- responseProperties.headers["content-length"] || 0;
568
- const { effectiveRecvDataLength, remoteWindowSize } =
569
- http2Stream.session.state;
570
- if (
571
- effectiveRecvDataLength + responseLength >
572
- remoteWindowSize
573
- ) {
574
- addRequestLog(requestNode, {
575
- type: "debug",
576
- value: `Aborting stream to prevent exceeding remoteWindowSize`,
577
- });
578
- abortController.abort();
579
- }
580
- }
729
+ if (preventedByService) {
730
+ pushRequestLogger.debug(
731
+ `response push prevented by "${preventedByService.name}" service`,
732
+ );
733
+ return;
581
734
  }
582
- await sendResponse({
583
- signal: pushResponseOperation.signal,
584
- request: pushRequest,
585
- requestNode,
586
- responseStream: pushStream,
587
- responseProperties,
588
- });
589
- } finally {
590
- await pushResponseOperation.end();
591
- }
592
- });
593
- };
594
735
 
595
- const handleRequest = async (request, { requestNode }) => {
596
- let requestReceivedMeasure;
597
- if (serverTiming) {
598
- requestReceivedMeasure = performance.now();
599
- }
600
- addRequestLog(requestNode, {
601
- type: "info",
602
- value: request.parent
603
- ? `Push ${request.resource}`
604
- : `${request.method} ${request.url}`,
605
- });
606
- const warn = (value) => {
607
- addRequestLog(requestNode, {
608
- type: "warn",
609
- value,
610
- });
611
- };
736
+ const http2Stream = nodeResponse.stream;
612
737
 
613
- let requestWaitingTimeout;
614
- if (requestWaitingMs) {
615
- requestWaitingTimeout = setTimeout(
616
- () => requestWaitingCallback({ request, warn, requestWaitingMs }),
617
- requestWaitingMs,
618
- ).unref();
619
- }
738
+ // being able to push a stream is nice to have
739
+ // so when it fails it's not critical
740
+ const onPushStreamError = (e) => {
741
+ pushRequestLogger.error(
742
+ createDetailedMessage(
743
+ `An error occured while pushing a stream to the response for ${request.resource}`,
744
+ {
745
+ "error stack": e.stack,
746
+ },
747
+ ),
748
+ );
749
+ };
620
750
 
621
- serviceController.callHooks(
622
- "redirectRequest",
623
- request,
624
- { warn },
625
- (newRequestProperties) => {
626
- if (newRequestProperties) {
627
- request = applyRedirectionToRequest(request, {
628
- original: request.original || request,
629
- previous: request,
630
- ...newRequestProperties,
751
+ // not aborted, let's try to push a stream into that response
752
+ // https://nodejs.org/docs/latest-v16.x/api/http2.html#http2streampushstreamheaders-options-callback
753
+ let pushStream;
754
+ try {
755
+ pushStream = await new Promise((resolve, reject) => {
756
+ http2Stream.pushStream(
757
+ {
758
+ ":path": path,
759
+ ...(method ? { ":method": method } : {}),
760
+ },
761
+ async (
762
+ error,
763
+ pushStream,
764
+ // headers
765
+ ) => {
766
+ if (error) {
767
+ reject(error);
768
+ }
769
+ resolve(pushStream);
770
+ },
771
+ );
631
772
  });
773
+ } catch (e) {
774
+ onPushStreamError(e);
775
+ return;
632
776
  }
633
- },
634
- );
635
777
 
636
- let handleRequestReturnValue;
637
- let errorWhileHandlingRequest = null;
638
- let handleRequestTimings = serverTiming ? {} : null;
639
-
640
- let timeout;
641
- const timeoutPromise = new Promise((resolve) => {
642
- timeout = setTimeout(() => {
643
- resolve({
644
- // the correct status code should be 500 because it's
645
- // we don't really know what takes time
646
- // in practice it's often because server is trying to reach an other server
647
- // that is not responding so 504 is more correct
648
- status: 504,
649
- statusText: `server timeout after ${
650
- responseTimeout / 1000
651
- }s waiting to handle request`,
778
+ const abortController = new AbortController();
779
+ // It's possible to get NGHTTP2_REFUSED_STREAM errors here
780
+ // https://github.com/nodejs/node/issues/20824
781
+ const pushErrorCallback = (error) => {
782
+ onPushStreamError(error);
783
+ abortController.abort();
784
+ };
785
+ pushStream.on("error", pushErrorCallback);
786
+ sendResponseOperation.addEndCallback(() => {
787
+ pushStream.removeListener("error", onPushStreamError);
652
788
  });
653
- }, responseTimeout);
654
- });
655
- const handleRequestPromise = serviceController.callAsyncHooksUntil(
656
- "handleRequest",
657
- request,
658
- {
659
- timing: handleRequestTimings,
660
- warn,
661
- pushResponse: async ({ path, method }) => {
662
- if (typeof path !== "string" || path[0] !== "/") {
663
- addRequestLog(requestNode, {
664
- type: "warn",
665
- value: `response push ignored because path is invalid (must be a string starting with "/", found ${path})`,
666
- });
667
- return;
668
- }
669
- if (!request.http2) {
670
- addRequestLog(requestNode, {
671
- type: "warn",
672
- value: `response push ignored because request is not http2`,
673
- });
674
- return;
675
- }
676
- const canPushStream = testCanPushStream(nodeResponse.stream);
677
- if (!canPushStream.can) {
678
- addRequestLog(requestNode, {
679
- type: "debug",
680
- value: `response push ignored because ${canPushStream.reason}`,
681
- });
682
- return;
683
- }
684
789
 
685
- let preventedByService = null;
686
- const prevent = () => {
687
- preventedByService = serviceController.getCurrentService();
688
- };
689
- serviceController.callHooksUntil(
690
- "onResponsePush",
691
- { path, method },
692
- {
693
- request,
694
- warn,
695
- prevent,
696
- },
697
- () => preventedByService,
698
- );
699
- if (preventedByService) {
700
- addRequestLog(requestNode, {
701
- type: "debug",
702
- value: `response push prevented by "${preventedByService.name}" service`,
703
- });
704
- return;
705
- }
790
+ await sendResponseOperation.withSignal(async (signal) => {
791
+ const pushResponseOperation = Abort.startOperation();
792
+ pushResponseOperation.addAbortSignal(signal);
793
+ pushResponseOperation.addAbortSignal(abortController.signal);
706
794
 
707
- const requestChildNode = { logs: [], children: [] };
708
- requestNode.children.push(requestChildNode);
709
- await pushResponse(
710
- { path, method },
711
- {
712
- requestNode: requestChildNode,
713
- parentHttp2Stream: nodeResponse.stream,
714
- },
715
- );
716
- },
717
- },
718
- );
719
- try {
720
- handleRequestReturnValue = await Promise.race([
721
- timeoutPromise,
722
- handleRequestPromise,
723
- ]);
724
- } catch (e) {
725
- errorWhileHandlingRequest = e;
726
- }
727
- clearTimeout(timeout);
728
-
729
- let responseProperties;
730
- if (errorWhileHandlingRequest) {
731
- if (
732
- errorWhileHandlingRequest.name === "AbortError" &&
733
- request.signal.aborted
734
- ) {
735
- responseProperties = { requestAborted: true };
736
- } else {
737
- // internal error, create 500 response
738
- if (
739
- // stopOnInternalError stops server only if requestToResponse generated
740
- // a non controlled error (internal error).
741
- // if requestToResponse gracefully produced a 500 response (it did not throw)
742
- // then we can assume we are still in control of what we are doing
743
- stopOnInternalError
744
- ) {
745
- // il faudrais pouvoir stop que les autres response ?
746
- stop(STOP_REASON_INTERNAL_ERROR);
747
- }
748
- const handleErrorReturnValue =
749
- await serviceController.callAsyncHooksUntil(
750
- "handleError",
751
- errorWhileHandlingRequest,
752
- {
753
- request,
754
- warn,
755
- },
756
- );
757
- if (!handleErrorReturnValue) {
758
- throw errorWhileHandlingRequest;
759
- }
760
- addRequestLog(requestNode, {
761
- type: "error",
762
- value: createDetailedMessage(
763
- `internal error while handling request`,
764
- {
765
- "error stack": errorWhileHandlingRequest.stack,
766
- },
767
- ),
768
- });
769
- responseProperties = composeTwoResponses(
770
- {
771
- status: 500,
772
- statusText: "Internal Server Error",
773
- headers: {
774
- // ensure error are not cached
775
- "cache-control": "no-store",
776
- "content-type": "text/plain",
777
- },
778
- },
779
- handleErrorReturnValue,
780
- );
781
- }
782
- } else {
783
- const {
784
- status = 501,
785
- statusText,
786
- statusMessage,
787
- headers = {},
788
- body,
789
- ...rest
790
- } = handleRequestReturnValue || {};
791
- responseProperties = {
792
- status,
793
- statusText,
794
- statusMessage,
795
- headers,
796
- body,
797
- ...rest,
798
- };
799
- }
800
-
801
- if (serverTiming) {
802
- const responseReadyMeasure = performance.now();
803
- const timeToStartResponding =
804
- responseReadyMeasure - requestReceivedMeasure;
805
- const serverTiming = {
806
- ...handleRequestTimings,
807
- ...responseProperties.timing,
808
- "time to start responding": timeToStartResponding,
809
- };
810
- responseProperties.headers = composeTwoHeaders(
811
- responseProperties.headers,
812
- timingToServerTimingResponseHeaders(serverTiming),
813
- );
814
- }
815
- if (requestWaitingMs) {
816
- clearTimeout(requestWaitingTimeout);
817
- }
818
- if (
819
- request.method !== "HEAD" &&
820
- responseProperties.headers["content-length"] > 0 &&
821
- !responseProperties.body
822
- ) {
823
- addRequestLog(requestNode, {
824
- type: "warn",
825
- value: `content-length header is ${responseProperties.headers["content-length"]} but body is empty`,
826
- });
827
- }
828
- serviceController.callHooks(
829
- "injectResponseHeaders",
830
- responseProperties,
831
- {
832
- request,
833
- warn,
834
- },
835
- (returnValue) => {
836
- if (returnValue) {
837
- responseProperties.headers = composeTwoHeaders(
838
- responseProperties.headers,
839
- returnValue,
840
- );
841
- }
842
- },
843
- );
844
- serviceController.callHooks("responseReady", responseProperties, {
845
- request,
846
- warn,
847
- });
848
- return responseProperties;
849
- };
850
-
851
- const sendResponse = async ({
852
- signal,
853
- request,
854
- requestNode,
855
- responseStream,
856
- responseProperties,
857
- }) => {
858
- // When "pushResponse" is called and the parent response has no body
859
- // the parent response is immediatly ended. It means child responses (pushed streams)
860
- // won't get a chance to be pushed.
861
- // To let a chance to pushed streams we wait a little before sending the response
862
- const ignoreBody = request.method === "HEAD";
863
- const bodyIsEmpty = !responseProperties.body || ignoreBody;
864
- if (bodyIsEmpty && requestNode.children.length > 0) {
865
- await new Promise((resolve) => setTimeout(resolve));
866
- }
795
+ const pushRequest = createPushRequest(request, {
796
+ signal: pushResponseOperation.signal,
797
+ pathname: path,
798
+ method,
799
+ logger: pushRequestLogger,
800
+ });
867
801
 
868
- await writeNodeResponse(responseStream, responseProperties, {
869
- signal,
870
- ignoreBody,
871
- onAbort: () => {
872
- addRequestLog(requestNode, {
873
- type: "info",
874
- value: `response aborted`,
875
- });
876
- onRequestHandled(requestNode);
877
- },
878
- onError: (error) => {
879
- addRequestLog(requestNode, {
880
- type: "error",
881
- value: createDetailedMessage(
882
- `An error occured while sending response`,
883
- {
884
- "error stack": error.stack,
885
- },
886
- ),
887
- });
888
- onRequestHandled(requestNode);
889
- },
890
- onHeadersSent: ({ status, statusText }) => {
891
- const statusType = statusToType(status);
892
- addRequestLog(requestNode, {
893
- type:
894
- status === 404 && request.pathname === "/favicon.ico"
895
- ? "debug"
896
- : {
897
- information: "info",
898
- success: "info",
899
- redirection: "info",
900
- client_error: "warn",
901
- server_error: "error",
902
- }[statusType],
903
- value: `${colorizeResponseStatus(status)} ${
904
- responseProperties.statusMessage || statusText
905
- }`,
802
+ try {
803
+ const responseProperties = await getResponseProperties(
804
+ pushRequest,
805
+ {
806
+ pushResponse: () => {
807
+ pushRequest.logger.warn(
808
+ `response push ignored because nested push is not supported`,
809
+ );
810
+ },
811
+ },
812
+ );
813
+ if (!abortController.signal.aborted) {
814
+ if (pushStream.destroyed) {
815
+ abortController.abort();
816
+ } else if (!http2Stream.pushAllowed) {
817
+ abortController.abort();
818
+ } else if (responseProperties.requestAborted) {
819
+ } else {
820
+ const responseLength =
821
+ responseProperties.headers["content-length"] || 0;
822
+ const { effectiveRecvDataLength, remoteWindowSize } =
823
+ http2Stream.session.state;
824
+ if (
825
+ effectiveRecvDataLength + responseLength >
826
+ remoteWindowSize
827
+ ) {
828
+ pushRequest.logger.debug(
829
+ `Aborting stream to prevent exceeding remoteWindowSize`,
830
+ );
831
+ abortController.abort();
832
+ }
833
+ }
834
+ }
835
+ await sendResponse(pushStream, responseProperties, {
836
+ signal: pushResponseOperation.signal,
837
+ request: pushRequest,
838
+ });
839
+ } finally {
840
+ await pushResponseOperation.end();
841
+ }
906
842
  });
907
843
  },
908
- onEnd: () => {
909
- onRequestHandled(requestNode);
910
- },
911
844
  });
912
- };
913
-
914
- try {
915
- if (receiveRequestOperation.signal.aborted) {
916
- return;
845
+ const webSocketHandler = getWebSocketHandler(responseProperties);
846
+ if (webSocketHandler) {
847
+ throw new Error(
848
+ "unexpected websocketResponse received for request that does not want to be upgraded to websocket. A regular response was expected.",
849
+ );
917
850
  }
918
- const responseProperties = await handleRequest(request, {
919
- requestNode: rootRequestNode,
920
- });
921
- nodeRequest.resume();
922
851
  if (receiveRequestOperation.signal.aborted) {
923
852
  return;
924
853
  }
925
-
926
- // the node request readable stream is never closed because
927
- // the response headers contains "connection: keep-alive"
928
- // In this scenario we want to disable READABLE_STREAM_TIMEOUT warning
929
- if (responseProperties.headers.connection === "keep-alive") {
930
- clearTimeout(request.body.timeout);
931
- }
932
-
933
- await sendResponse({
854
+ await sendResponse(nodeResponse, responseProperties, {
934
855
  signal: sendResponseOperation.signal,
935
856
  request,
936
- requestNode: rootRequestNode,
937
- responseStream: nodeResponse,
938
- responseProperties,
939
857
  });
940
858
  } finally {
941
859
  await sendResponseOperation.end();
942
860
  }
943
861
  };
944
- const removeRequestListener = listenRequest(nodeServer, requestCallback);
862
+ const removeRequestListener = listenRequest(
863
+ nodeServer,
864
+ requestEventHandler,
865
+ );
945
866
  // ensure we don't try to handle new requests while server is stopping
946
867
  stopCallbackSet.add(removeRequestListener);
947
868
  }
948
869
 
949
870
  websocket: {
950
871
  // https://github.com/websockets/ws/blob/master/doc/ws.md#class-websocket
951
- const websocketHandlers = [];
952
- serviceController.services.forEach((service) => {
953
- const { handleWebsocket } = service;
954
- if (handleWebsocket) {
955
- websocketHandlers.push(handleWebsocket);
872
+ const webSocketOrigin = https
873
+ ? `wss://${hostname}:${port}`
874
+ : `ws://${hostname}:${port}`;
875
+ server.webSocketOrigin = webSocketOrigin;
876
+ const webSocketSet = new Set();
877
+ let upgradeRequestToWebSocketPromise;
878
+ let upgradeRequestToWebSocket;
879
+ const loadUpgradeRequestToWebSocket = async () => {
880
+ if (upgradeRequestToWebSocketPromise) {
881
+ await upgradeRequestToWebSocketPromise;
882
+ return;
956
883
  }
957
- });
958
- if (websocketHandlers.length > 0) {
959
- const websocketClients = new Set();
960
884
  const { WebSocketServer } = await import("ws");
961
- let websocketServer = new WebSocketServer({ noServer: true });
962
- const websocketOrigin = https
963
- ? `wss://${hostname}:${port}`
964
- : `ws://${hostname}:${port}`;
965
- server.websocketOrigin = websocketOrigin;
966
- const upgradeCallback = (nodeRequest, socket, head) => {
967
- websocketServer.handleUpgrade(
968
- nodeRequest,
969
- socket,
970
- head,
971
- async (websocket) => {
972
- const websocketAbortController = new AbortController();
973
- websocketClients.add(websocket);
974
- websocket.signal = websocketAbortController.signal;
975
- websocket.once("close", () => {
976
- websocketClients.delete(websocket);
977
- websocketAbortController.abort();
978
- });
979
- const request = fromNodeRequest(nodeRequest, {
980
- signal: stopAbortSignal,
981
- serverOrigin: websocketOrigin,
982
- requestBodyLifetime,
983
- });
984
- serviceController.callAsyncHooksUntil(
985
- "handleWebsocket",
986
- websocket,
987
- {
988
- request,
989
- },
990
- );
991
- },
992
- );
993
- };
994
-
995
- // see server-polyglot.js, upgrade must be listened on https server when used
996
- const facadeServer = nodeServer._tlsServer || nodeServer;
997
- const removeUpgradeCallback = listenEvent(
998
- facadeServer,
999
- "upgrade",
1000
- upgradeCallback,
1001
- );
1002
- stopCallbackSet.add(removeUpgradeCallback);
885
+ let webSocketServer = new WebSocketServer({ noServer: true });
1003
886
  stopCallbackSet.add(() => {
1004
- websocketClients.forEach((websocketClient) => {
1005
- websocketClient.close();
887
+ webSocketServer.close();
888
+ webSocketServer = null;
889
+ });
890
+ upgradeRequestToWebSocket = async ({ nodeRequest, socket, head }) => {
891
+ const websocket = await new Promise((resolve) => {
892
+ webSocketServer.handleUpgrade(nodeRequest, socket, head, resolve);
1006
893
  });
1007
- websocketClients.clear();
1008
- websocketServer.close();
1009
- websocketServer = null;
894
+ return websocket;
895
+ };
896
+ };
897
+ // https://github.com/websockets/ws/blob/b92745a9d6760e6b4b2394bfac78cbcd258a8c8d/lib/websocket-server.js#L491
898
+ const upgradeEventHandler = async (nodeRequest, socket, head) => {
899
+ let request = fromNodeRequest(nodeRequest, {
900
+ signal: stopAbortSignal,
901
+ serverOrigin,
902
+ requestBodyLifetime,
903
+ logger,
904
+ nagle,
1010
905
  });
1011
- }
906
+ const [receiveRequestOperation, sendResponseOperation] =
907
+ prepareHandleRequestOperations(nodeRequest, socket);
908
+ const responseProperties = await getResponseProperties(request, {
909
+ pushResponse: () => {
910
+ request.logger.warn(
911
+ `pushResponse ignored because it's not supported in websocket`,
912
+ );
913
+ },
914
+ });
915
+ if (receiveRequestOperation.signal.aborted) {
916
+ return;
917
+ }
918
+ if (responseProperties.status !== 101) {
919
+ await sendResponse(socket, responseProperties, {
920
+ signal: sendResponseOperation.signal,
921
+ request,
922
+ });
923
+ return;
924
+ }
925
+ const webSocketHandler = getWebSocketHandler(responseProperties);
926
+ if (!webSocketHandler) {
927
+ throw new Error(
928
+ "unexpected response received for request that wants to be upgraded to websocket. A webSocketResponse was expected.",
929
+ );
930
+ }
931
+ if (!upgradeRequestToWebSocket) {
932
+ await loadUpgradeRequestToWebSocket();
933
+ }
934
+ if (sendResponseOperation.signal.aborted) {
935
+ return;
936
+ }
937
+ const webSocket = await upgradeRequestToWebSocket({
938
+ nodeRequest,
939
+ socket,
940
+ head,
941
+ });
942
+ if (sendResponseOperation.signal.aborted) {
943
+ webSocket.destroy();
944
+ return;
945
+ }
946
+ const webSocketAbortController = new AbortController();
947
+ webSocketSet.add(webSocket);
948
+ webSocket.once("close", () => {
949
+ webSocketSet.delete(webSocket);
950
+ webSocketAbortController.abort();
951
+ });
952
+ request.logger.onHeadersSent({
953
+ status: 101,
954
+ statusText: "Switching Protocols",
955
+ });
956
+ request.logger.end();
957
+ let websocketHandlerReturnValue = await webSocketHandler(webSocket);
958
+ if (typeof websocketHandlerReturnValue === "function") {
959
+ webSocket.once("close", () => {
960
+ websocketHandlerReturnValue();
961
+ websocketHandlerReturnValue = undefined;
962
+ });
963
+ }
964
+ return;
965
+ };
966
+ // see server-polyglot.js, upgrade must be listened on https server when used
967
+ const facadeServer = nodeServer._tlsServer || nodeServer;
968
+ const removeUpgradeCallback = listenEvent(
969
+ facadeServer,
970
+ "upgrade",
971
+ upgradeEventHandler,
972
+ );
973
+ stopCallbackSet.add(removeUpgradeCallback);
974
+ stopCallbackSet.add(() => {
975
+ for (const websocket of webSocketSet) {
976
+ websocket.close();
977
+ }
978
+ webSocketSet.clear();
979
+ });
1012
980
  }
1013
981
 
1014
982
  if (startLog) {