@jsenv/server 15.5.3 → 16.0.1
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/README.md +79 -86
- package/dist/html/4xx.html +196 -0
- package/dist/html/500.html +17 -0
- package/dist/html/route_inspector.html +2435 -0
- package/dist/js/formidable.js +17 -0
- package/dist/js/vendors.js +2001 -0
- package/dist/js/ws.js +6863 -0
- package/dist/jsenv_server.js +8829 -0
- package/package.json +19 -9
- package/src/content_negotiation/pick_content_version.js +63 -0
- package/src/{fetch_filesystem.js → filesystem/fetch_filesystem.js} +57 -72
- package/src/{serve_directory.js → filesystem/serve_directory.js} +1 -2
- package/src/interfacing_with_node/from_node_request.js +455 -0
- package/src/interfacing_with_node/get_observable_value_type.js +43 -0
- package/src/interfacing_with_node/observable.js +91 -75
- package/src/interfacing_with_node/observable_from.js +34 -0
- package/src/interfacing_with_node/observable_from_file_handle.js +30 -0
- package/src/interfacing_with_node/observable_from_node_stream.js +44 -46
- package/src/interfacing_with_node/observable_from_node_web_readable_stream.js +32 -0
- package/src/interfacing_with_node/observable_from_promise.js +13 -0
- package/src/interfacing_with_node/write_node_response.js +80 -35
- package/src/internal/colorizeResponseStatus.js +2 -0
- package/src/internal/headers_composition.js +42 -15
- package/src/internal/multiple-header.js +27 -12
- package/src/internal/object_composition.js +2 -2
- package/src/internal/response_composition.js +16 -3
- package/src/main.js +15 -11
- package/src/progressive_response.js +38 -0
- package/src/replace_placeholder_in_html.js +12 -0
- package/src/request_body_handling.js +22 -0
- package/src/router/router.js +878 -0
- package/src/server_events.js +431 -0
- package/src/service_controller.js +79 -73
- package/src/services/autoreload_on_server_restart/client/autoreload_on_server_restart.js +125 -0
- package/src/services/autoreload_on_server_restart/jsenv_service_autoreload_on_server_restart.js +45 -0
- package/src/services/cors/jsenv_service_cors.js +10 -18
- package/src/services/default_body_4xx_5xx/client/4xx.html +65 -0
- package/src/services/default_body_4xx_5xx/jsenv_service_default_body_4xx_5xx.js +82 -0
- package/src/services/error_handler/client/500.html +17 -0
- package/src/services/error_handler/jsenv_service_error_handler.js +47 -28
- package/src/services/internal_client_files/jsenv_service_internal_client_files.js +34 -0
- package/src/services/open_file/jsenv_service_open_file.js +37 -0
- package/src/services/route_inspector/client/code_block.js +215 -0
- package/src/services/route_inspector/client/route_inspector.html +1363 -0
- package/src/services/route_inspector/jsenv_service_route_inspector.js +39 -0
- package/src/start_server.js +575 -607
- package/src/web_socket_response.js +53 -0
- package/src/from_fetch_response.js +0 -15
- package/src/interfacing_with_node/body.js +0 -81
- package/src/interfacing_with_node/request_factory.js +0 -125
- package/src/readRequestBody.js +0 -32
- package/src/server_timing/timing_measure.js +0 -26
- package/src/service_composition/path_to_regexp.js +0 -436
- package/src/service_composition/routing.js +0 -31
- package/src/sse/sse_room.js +0 -313
package/src/start_server.js
CHANGED
|
@@ -8,13 +8,13 @@ import {
|
|
|
8
8
|
applyRedirectionToRequest,
|
|
9
9
|
createPushRequest,
|
|
10
10
|
fromNodeRequest,
|
|
11
|
-
} from "./interfacing_with_node/
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
} from "./internal/
|
|
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 {
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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 =
|
|
377
|
-
|
|
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
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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
|
-
|
|
444
|
-
|
|
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
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
{
|
|
499
|
-
|
|
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
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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
|
-
|
|
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
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
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
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
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
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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
|
-
|
|
686
|
-
const
|
|
687
|
-
|
|
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
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
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
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
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
|
-
|
|
915
|
-
|
|
916
|
-
|
|
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(
|
|
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
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
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
|
|
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
|
-
|
|
1005
|
-
|
|
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
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
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) {
|