@reckona/mreact-router 0.0.203 → 0.0.205

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 (140) hide show
  1. package/README.md +53 -9
  2. package/dist/actions.d.ts +6 -0
  3. package/dist/actions.d.ts.map +1 -1
  4. package/dist/actions.js +70 -10
  5. package/dist/actions.js.map +1 -1
  6. package/dist/adapters/aws-lambda.d.ts +15 -4
  7. package/dist/adapters/aws-lambda.d.ts.map +1 -1
  8. package/dist/adapters/aws-lambda.js +301 -63
  9. package/dist/adapters/aws-lambda.js.map +1 -1
  10. package/dist/adapters/cloudflare.d.ts +2 -1
  11. package/dist/adapters/cloudflare.d.ts.map +1 -1
  12. package/dist/adapters/cloudflare.js +18 -10
  13. package/dist/adapters/cloudflare.js.map +1 -1
  14. package/dist/adapters/edge.d.ts +1 -1
  15. package/dist/adapters/edge.d.ts.map +1 -1
  16. package/dist/adapters/edge.js.map +1 -1
  17. package/dist/adapters/node.d.ts +5 -3
  18. package/dist/adapters/node.d.ts.map +1 -1
  19. package/dist/adapters/node.js +2 -1
  20. package/dist/adapters/node.js.map +1 -1
  21. package/dist/adapters/static.d.ts.map +1 -1
  22. package/dist/adapters/static.js +24 -2
  23. package/dist/adapters/static.js.map +1 -1
  24. package/dist/build.d.ts +1 -0
  25. package/dist/build.d.ts.map +1 -1
  26. package/dist/build.js +91 -5
  27. package/dist/build.js.map +1 -1
  28. package/dist/built-runtime.d.ts +2 -0
  29. package/dist/built-runtime.d.ts.map +1 -1
  30. package/dist/built-runtime.js +16 -0
  31. package/dist/built-runtime.js.map +1 -1
  32. package/dist/cache.d.ts +3 -1
  33. package/dist/cache.d.ts.map +1 -1
  34. package/dist/cache.js +211 -13
  35. package/dist/cache.js.map +1 -1
  36. package/dist/cli.js +12 -1
  37. package/dist/cli.js.map +1 -1
  38. package/dist/client.d.ts.map +1 -1
  39. package/dist/client.js +32 -1
  40. package/dist/client.js.map +1 -1
  41. package/dist/config.d.ts +3 -0
  42. package/dist/config.d.ts.map +1 -1
  43. package/dist/config.js +21 -2
  44. package/dist/config.js.map +1 -1
  45. package/dist/csp.d.ts +1 -0
  46. package/dist/csp.d.ts.map +1 -1
  47. package/dist/csp.js +11 -6
  48. package/dist/csp.js.map +1 -1
  49. package/dist/dehydrate-policy.d.ts +3 -0
  50. package/dist/dehydrate-policy.d.ts.map +1 -0
  51. package/dist/dehydrate-policy.js +16 -0
  52. package/dist/dehydrate-policy.js.map +1 -0
  53. package/dist/dev-server.d.ts +7 -2
  54. package/dist/dev-server.d.ts.map +1 -1
  55. package/dist/dev-server.js +120 -11
  56. package/dist/dev-server.js.map +1 -1
  57. package/dist/http.d.ts +1 -1
  58. package/dist/http.d.ts.map +1 -1
  59. package/dist/http.js +147 -7
  60. package/dist/http.js.map +1 -1
  61. package/dist/index.d.ts +4 -3
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +1 -0
  64. package/dist/index.js.map +1 -1
  65. package/dist/logger.d.ts +22 -1
  66. package/dist/logger.d.ts.map +1 -1
  67. package/dist/logger.js +22 -8
  68. package/dist/logger.js.map +1 -1
  69. package/dist/metadata.d.ts +8 -0
  70. package/dist/metadata.d.ts.map +1 -1
  71. package/dist/metadata.js +60 -22
  72. package/dist/metadata.js.map +1 -1
  73. package/dist/navigation-marker.d.ts.map +1 -1
  74. package/dist/navigation-marker.js +72 -9
  75. package/dist/navigation-marker.js.map +1 -1
  76. package/dist/navigation.d.ts +1 -0
  77. package/dist/navigation.d.ts.map +1 -1
  78. package/dist/navigation.js +1 -1
  79. package/dist/navigation.js.map +1 -1
  80. package/dist/node-server.d.ts +5 -2
  81. package/dist/node-server.d.ts.map +1 -1
  82. package/dist/node-server.js +75 -6
  83. package/dist/node-server.js.map +1 -1
  84. package/dist/prerender-entry.d.ts +14 -0
  85. package/dist/prerender-entry.d.ts.map +1 -1
  86. package/dist/prerender-entry.js +40 -3
  87. package/dist/prerender-entry.js.map +1 -1
  88. package/dist/render.d.ts +9 -1
  89. package/dist/render.d.ts.map +1 -1
  90. package/dist/render.js +102 -123
  91. package/dist/render.js.map +1 -1
  92. package/dist/serve.d.ts +8 -2
  93. package/dist/serve.d.ts.map +1 -1
  94. package/dist/serve.js +22 -20
  95. package/dist/serve.js.map +1 -1
  96. package/dist/session.d.ts.map +1 -1
  97. package/dist/session.js +52 -19
  98. package/dist/session.js.map +1 -1
  99. package/dist/upgrade.d.ts +59 -2
  100. package/dist/upgrade.d.ts.map +1 -1
  101. package/dist/upgrade.js +282 -1
  102. package/dist/upgrade.js.map +1 -1
  103. package/dist/vite-config.d.ts +2 -0
  104. package/dist/vite-config.d.ts.map +1 -1
  105. package/dist/vite-config.js +2 -0
  106. package/dist/vite-config.js.map +1 -1
  107. package/dist/vite.d.ts +4 -1
  108. package/dist/vite.d.ts.map +1 -1
  109. package/dist/vite.js +117 -6
  110. package/dist/vite.js.map +1 -1
  111. package/package.json +11 -11
  112. package/src/actions.ts +81 -11
  113. package/src/adapters/aws-lambda.ts +426 -115
  114. package/src/adapters/cloudflare.ts +29 -13
  115. package/src/adapters/edge.ts +2 -0
  116. package/src/adapters/node.ts +8 -10
  117. package/src/adapters/static.ts +31 -2
  118. package/src/build.ts +117 -10
  119. package/src/built-runtime.ts +28 -0
  120. package/src/cache.ts +262 -15
  121. package/src/cli.ts +14 -1
  122. package/src/client.ts +32 -1
  123. package/src/config.ts +43 -6
  124. package/src/csp.ts +14 -11
  125. package/src/dehydrate-policy.ts +23 -0
  126. package/src/dev-server.ts +188 -15
  127. package/src/http.ts +172 -6
  128. package/src/index.ts +14 -1
  129. package/src/logger.ts +47 -10
  130. package/src/metadata.ts +125 -60
  131. package/src/navigation-marker.ts +79 -9
  132. package/src/navigation.ts +1 -1
  133. package/src/node-server.ts +128 -22
  134. package/src/prerender-entry.ts +52 -3
  135. package/src/render.ts +807 -753
  136. package/src/serve.ts +49 -56
  137. package/src/session.ts +56 -25
  138. package/src/upgrade.ts +402 -3
  139. package/src/vite-config.ts +6 -0
  140. package/src/vite.ts +157 -5
@@ -1,12 +1,13 @@
1
- import { Buffer } from "node:buffer";
1
+ import { Buffer, isUtf8 } from "node:buffer";
2
2
  import { createHash } from "node:crypto";
3
3
  import { access, lstat, mkdir, readFile, readlink, symlink } from "node:fs/promises";
4
4
  import { tmpdir } from "node:os";
5
5
  import { dirname, join } from "node:path";
6
6
  import { normalizeBuiltAppRuntimePreloadStrategy, } from "../preload-policy.js";
7
7
  import { emitRouterLog, logDurationMs, logError, logNow, requestLogFields, } from "../logger.js";
8
- import { createBuiltRequestRuntime, preloadBuiltAppRuntime, resolveRequestHost, warnIfImplicitHostTrust, } from "../serve.js";
9
- const invalidAwsLambdaHttpEventV2Diagnostic = 'Expected an AWS Lambda HTTP API payload format 2.0 event with rawPath and requestContext.http.method.';
8
+ import { createBuiltRequestRuntime, preloadBuiltAppRuntime, resolveRequestHost, } from "../serve.js";
9
+ let warnedImplicitAwsLambdaHostPolicy = false;
10
+ const invalidAwsLambdaHttpEventV2Diagnostic = "Expected an AWS Lambda HTTP API payload format 2.0 event with rawPath and requestContext.http.method.";
10
11
  function invalidAwsLambdaHttpEventV2Result() {
11
12
  return {
12
13
  body: invalidAwsLambdaHttpEventV2Diagnostic,
@@ -27,7 +28,7 @@ function invalidAwsLambdaHttpEventV2Response() {
27
28
  * The handler materializes runtime files under `/tmp` by default, starts middleware-focused background preload, applies the generated import policy when requested, and returns API Gateway v2 response objects.
28
29
  */
29
30
  export function createAwsLambdaRequestHandler(options) {
30
- warnIfImplicitHostTrust(options);
31
+ warnIfImplicitAwsLambdaHostPolicy(options);
31
32
  let handler;
32
33
  return async (event) => {
33
34
  try {
@@ -51,7 +52,7 @@ export function createAwsLambdaRequestHandler(options) {
51
52
  * Invalid events are rejected before runtime materialization or preload work begins.
52
53
  */
53
54
  export async function createPreloadedAwsLambdaRequestHandler(options) {
54
- warnIfImplicitHostTrust(options);
55
+ warnIfImplicitAwsLambdaHostPolicy(options);
55
56
  let handler;
56
57
  return async (event) => {
57
58
  try {
@@ -75,7 +76,7 @@ export async function createPreloadedAwsLambdaRequestHandler(options) {
75
76
  * Call this during controlled Lambda initialization only when the extra startup work is preferable to first-valid-request latency. Request handlers always validate events before they start runtime work.
76
77
  */
77
78
  export async function warmAwsLambdaRuntime(options) {
78
- warnIfImplicitHostTrust(options);
79
+ warnIfImplicitAwsLambdaHostPolicy(options);
79
80
  const runtimeDir = await prepareAwsLambdaRuntimeDir(options);
80
81
  await preloadAwsLambdaRuntime(options, runtimeDir);
81
82
  }
@@ -89,15 +90,17 @@ function createAwsLambdaRequestHandlerFromRuntime(options, runtimeDirPromise, ru
89
90
  }
90
91
  const startedAt = logNow();
91
92
  const phases = createAwsLambdaTimingPhases(options);
92
- const eventToRequestStartedAt = phaseStartedAt(phases);
93
- const request = eventToRequest(event, options);
94
- finishPhase(phases, eventToRequestStartedAt, "eventToRequestMs");
95
- const logFields = requestLogFields(request, "aws-lambda");
96
- emitRouterLog(options.logger, "info", {
97
- ...logFields,
98
- type: "router:request:start",
99
- });
93
+ let request;
94
+ let logFields = awsLambdaEventLogFields(event);
100
95
  try {
96
+ const eventToRequestStartedAt = phaseStartedAt(phases);
97
+ request = eventToRequest(event, options);
98
+ finishPhase(phases, eventToRequestStartedAt, "eventToRequestMs");
99
+ logFields = requestLogFields(request, "aws-lambda");
100
+ emitRouterLog(options.logger, "info", {
101
+ ...logFields,
102
+ type: "router:request:start",
103
+ });
101
104
  const runtimeDirStartedAt = phaseStartedAt(phases);
102
105
  const runtimeDir = await runtimeDirPromise;
103
106
  finishPhase(phases, runtimeDirStartedAt, "runtimeDirMs");
@@ -112,6 +115,7 @@ function createAwsLambdaRequestHandlerFromRuntime(options, runtimeDirPromise, ru
112
115
  });
113
116
  const preload = awsLambdaRenderPreload(options, runtimePreloadPromise, defaultPreloadMode);
114
117
  const response = await builtRuntime.render(request, {
118
+ dehydrateOptions: options.dehydrateOptions,
115
119
  instrumentation: options.instrumentation,
116
120
  logger: awsLambdaRenderLogger(options),
117
121
  onResponse: options.onResponse,
@@ -161,7 +165,7 @@ function createAwsLambdaRequestHandlerFromRuntime(options, runtimeDirPromise, ru
161
165
  * Use this with Lambda response streaming runtimes when HTML or route responses should be written progressively instead of buffered into an API Gateway result.
162
166
  */
163
167
  export function createAwsLambdaStreamingRequestHandler(options) {
164
- warnIfImplicitHostTrust(options);
168
+ warnIfImplicitAwsLambdaHostPolicy(options);
165
169
  const runtime = awsLambdaRuntime();
166
170
  let handler;
167
171
  return runtime.streamifyResponse(async (event, responseStream, context) => {
@@ -187,7 +191,7 @@ export function createAwsLambdaStreamingRequestHandler(options) {
187
191
  * Invalid events are rejected before runtime materialization or preload work begins.
188
192
  */
189
193
  export async function createPreloadedAwsLambdaStreamingRequestHandler(options) {
190
- warnIfImplicitHostTrust(options);
194
+ warnIfImplicitAwsLambdaHostPolicy(options);
191
195
  const runtime = awsLambdaRuntime();
192
196
  let handler;
193
197
  return runtime.streamifyResponse(async (event, responseStream, context) => {
@@ -307,15 +311,20 @@ function createAwsLambdaStreamingRequestHandlerFromRuntime(options, runtime, run
307
311
  }
308
312
  const startedAt = logNow();
309
313
  const phases = createAwsLambdaTimingPhases(options);
310
- const eventToRequestStartedAt = phaseStartedAt(phases);
311
- const request = eventToRequest(event, options);
312
- finishPhase(phases, eventToRequestStartedAt, "eventToRequestMs");
313
- const logFields = requestLogFields(request, "aws-lambda");
314
- emitRouterLog(options.logger, "info", {
315
- ...logFields,
316
- type: "router:request:start",
317
- });
314
+ const requestAbortController = new AbortController();
315
+ let request;
316
+ let requestLifecycle;
317
+ let logFields = awsLambdaEventLogFields(event);
318
318
  try {
319
+ const eventToRequestStartedAt = phaseStartedAt(phases);
320
+ request = eventToRequest(event, options, requestAbortController.signal);
321
+ requestLifecycle = observeLambdaStreamFailure(responseStream, requestAbortController);
322
+ finishPhase(phases, eventToRequestStartedAt, "eventToRequestMs");
323
+ logFields = requestLogFields(request, "aws-lambda");
324
+ emitRouterLog(options.logger, "info", {
325
+ ...logFields,
326
+ type: "router:request:start",
327
+ });
319
328
  const runtimeDirStartedAt = phaseStartedAt(phases);
320
329
  const runtimeDir = await runtimeDirPromise;
321
330
  finishPhase(phases, runtimeDirStartedAt, "runtimeDirMs");
@@ -330,6 +339,7 @@ function createAwsLambdaStreamingRequestHandlerFromRuntime(options, runtime, run
330
339
  });
331
340
  const preload = awsLambdaRenderPreload(options, runtimePreloadPromise, defaultPreloadMode);
332
341
  const response = await builtRuntime.render(request, {
342
+ dehydrateOptions: options.dehydrateOptions,
333
343
  instrumentation: options.instrumentation,
334
344
  logger: awsLambdaRenderLogger(options),
335
345
  onResponse: options.onResponse,
@@ -341,7 +351,7 @@ function createAwsLambdaStreamingRequestHandlerFromRuntime(options, runtime, run
341
351
  });
342
352
  finishPhase(phases, renderStartedAt, "renderMs");
343
353
  const responseStreamingStartedAt = phaseStartedAt(phases);
344
- await streamResponseToLambda(response, responseStream, runtime, phases);
354
+ await streamResponseToLambda(response, responseStream, runtime, phases, requestAbortController);
345
355
  finishPhase(phases, responseStreamingStartedAt, "responseStreamingMs");
346
356
  emitRouterLog(options.logger, "info", {
347
357
  ...logFields,
@@ -358,6 +368,9 @@ function createAwsLambdaStreamingRequestHandlerFromRuntime(options, runtime, run
358
368
  error: logError(error),
359
369
  type: "router:request:error",
360
370
  });
371
+ if (requestAbortController.signal.aborted) {
372
+ return;
373
+ }
361
374
  const payload = options.errorHandler
362
375
  ? options.errorHandler(error)
363
376
  : { body: "Internal Server Error", status: 500 };
@@ -368,11 +381,17 @@ function createAwsLambdaStreamingRequestHandlerFromRuntime(options, runtime, run
368
381
  },
369
382
  status: payload.status,
370
383
  }), options, request);
371
- await streamResponseToLambda(response, responseStream, runtime, phases);
384
+ await streamResponseToLambda(response, responseStream, runtime, phases, requestAbortController);
385
+ }
386
+ finally {
387
+ requestLifecycle?.dispose();
372
388
  }
373
389
  });
374
390
  }
375
391
  async function applyAwsLambdaResponseHook(response, options, request) {
392
+ if (request === undefined) {
393
+ return response;
394
+ }
376
395
  const hooked = await options.onResponse?.(response, { request });
377
396
  return hooked instanceof Response ? hooked : response;
378
397
  }
@@ -428,7 +447,7 @@ function isNodeErrorCode(error, code) {
428
447
  "code" in error &&
429
448
  error.code === code);
430
449
  }
431
- function eventToRequest(event, options) {
450
+ function eventToRequest(event, options, signal) {
432
451
  validateAwsLambdaHttpEventV2(event);
433
452
  const headers = eventHeaders(event);
434
453
  const rawHost = lambdaRequestHost(headers, options);
@@ -438,19 +457,51 @@ function eventToRequest(event, options) {
438
457
  hostPolicy: lambdaHostPolicy(options),
439
458
  rawHost: rawHost ?? undefined,
440
459
  });
441
- const protocol = lambdaRequestProtocol(event, headers, options);
442
- const rawPath = event.rawPath;
443
- const rawQueryString = event.rawQueryString === "" ? "" : `?${event.rawQueryString}`;
460
+ const protocol = lambdaRequestProtocol(headers, options);
444
461
  const method = event.requestContext.http.method;
445
462
  const init = {
446
463
  headers,
447
464
  method,
465
+ ...(signal === undefined ? {} : { signal }),
448
466
  };
449
467
  if (method !== "GET" && method !== "HEAD" && event.body !== undefined) {
450
- init.body =
451
- event.isBase64Encoded === true ? Buffer.from(event.body, "base64") : event.body;
468
+ init.body = event.isBase64Encoded === true ? Buffer.from(event.body, "base64") : event.body;
469
+ }
470
+ return new Request(lambdaRequestUrl(protocol, host, event.rawPath, event.rawQueryString), init);
471
+ }
472
+ function lambdaRequestUrl(protocol, host, rawPath, rawQueryString) {
473
+ if (hasInvalidLambdaRequestAuthorityCharacter(host)) {
474
+ throw new TypeError("AWS Lambda request Host is not a valid URL authority");
475
+ }
476
+ const url = new URL(`${protocol}://${host}`);
477
+ if (url.protocol !== `${protocol}:` ||
478
+ url.host === "" ||
479
+ url.username !== "" ||
480
+ url.password !== "" ||
481
+ url.pathname !== "/" ||
482
+ url.search !== "" ||
483
+ url.hash !== "") {
484
+ throw new TypeError("AWS Lambda request Host is not a valid URL authority");
485
+ }
486
+ url.pathname = rawPath;
487
+ url.search = rawQueryString;
488
+ return url;
489
+ }
490
+ function hasInvalidLambdaRequestAuthorityCharacter(host) {
491
+ for (const character of host) {
492
+ const codePoint = character.codePointAt(0) ?? 0;
493
+ if (character === "/" ||
494
+ character === "?" ||
495
+ character === "#" ||
496
+ character === "\\" ||
497
+ character === "@" ||
498
+ character.trim() === "" ||
499
+ codePoint < 0x20 ||
500
+ codePoint === 0x7f) {
501
+ return true;
502
+ }
452
503
  }
453
- return new Request(`${protocol}://${host}${rawPath}${rawQueryString}`, init);
504
+ return false;
454
505
  }
455
506
  function validateAwsLambdaHttpEventV2(event) {
456
507
  const candidate = event;
@@ -462,10 +513,12 @@ function validateAwsLambdaHttpEventV2(event) {
462
513
  typeof candidate.rawQueryString !== "string" ||
463
514
  typeof candidate.requestContext !== "object" ||
464
515
  candidate.requestContext === null ||
465
- typeof (candidate.requestContext.http) !== "object" ||
516
+ typeof candidate.requestContext.http !== "object" ||
466
517
  candidate.requestContext.http === null ||
467
- typeof (candidate.requestContext.http.method) !== "string" ||
468
- candidate.requestContext.http.method === "") {
518
+ typeof candidate.requestContext.http
519
+ .method !== "string" ||
520
+ candidate.requestContext.http.method ===
521
+ "") {
469
522
  throw new Error(invalidAwsLambdaHttpEventV2Diagnostic);
470
523
  }
471
524
  }
@@ -478,18 +531,34 @@ function lambdaRequestHost(headers, options) {
478
531
  function lambdaHostPolicy(options) {
479
532
  return options.hostPolicy ?? (process.env.NODE_ENV === "production" ? "strict" : undefined);
480
533
  }
481
- function lambdaRequestProtocol(event, headers, options) {
534
+ function warnIfImplicitAwsLambdaHostPolicy(options) {
535
+ if (process.env.NODE_ENV !== "production" ||
536
+ options.allowedHosts !== undefined ||
537
+ options.hostPolicy !== undefined ||
538
+ warnedImplicitAwsLambdaHostPolicy) {
539
+ return;
540
+ }
541
+ warnedImplicitAwsLambdaHostPolicy = true;
542
+ console.error('[mreact] AWS Lambda Host handling defaults to strict because neither allowedHosts nor hostPolicy is configured. Unlisted Host headers use the configured hostname or lambda.local. Set allowedHosts for public deployments or hostPolicy: "trusted-proxy" when a trusted reverse proxy normalizes Host.');
543
+ }
544
+ function lambdaRequestProtocol(headers, options) {
482
545
  if (options.trustForwardedProto === true) {
483
546
  return normalizeRequestProtocol(firstForwardedValue(headers.get("x-forwarded-proto")));
484
547
  }
485
- return normalizeRequestProtocol(event.requestContext?.http?.protocol);
548
+ return "https";
486
549
  }
487
550
  function normalizeRequestProtocol(value) {
488
551
  if (value === undefined || value === "") {
489
552
  return "https";
490
553
  }
491
- const normalized = value.toLowerCase();
492
- return normalized === "http" || normalized.startsWith("http/") ? "http" : "https";
554
+ return value.toLowerCase() === "http" ? "http" : "https";
555
+ }
556
+ function awsLambdaEventLogFields(event) {
557
+ return {
558
+ method: event.requestContext.http.method,
559
+ path: event.rawPath,
560
+ runtime: "aws-lambda",
561
+ };
493
562
  }
494
563
  function eventHeaders(event) {
495
564
  const headers = new Headers();
@@ -525,7 +594,7 @@ async function responseToLambdaResult(response, phases) {
525
594
  addPhaseDuration(phases, streamDrainStartedAt, "streamDrainMs");
526
595
  const bodyEncodeStartedAt = phaseStartedAt(phases);
527
596
  const contentType = response.headers.get("content-type");
528
- const text = isTextContentType(contentType);
597
+ const text = isUtf8TextContentType(contentType) && isUtf8(bytes);
529
598
  const result = {
530
599
  body: text ? bytes.toString("utf8") : bytes.toString("base64"),
531
600
  ...(cookies.length === 0 ? {} : { cookies }),
@@ -605,26 +674,58 @@ function emitAwsLambdaTiming(options, logFields, status, startedAt, phases) {
605
674
  type: "router:request:timing",
606
675
  });
607
676
  }
608
- async function streamResponseToLambda(response, responseStream, runtime, phases) {
677
+ async function streamResponseToLambda(response, responseStream, runtime, phases, requestAbortController) {
678
+ if (isLambdaStreamingAborted(requestAbortController)) {
679
+ return;
680
+ }
609
681
  const stream = runtime.HttpResponseStream.from(responseStream, responseStreamingMetadata(response));
682
+ if (isLambdaStreamingAborted(requestAbortController)) {
683
+ return;
684
+ }
610
685
  try {
611
686
  if (response.body === null) {
612
687
  stream.end();
613
688
  return;
614
689
  }
615
690
  const reader = response.body.getReader();
616
- while (true) {
617
- const streamWaitStartedAt = phaseStartedAt(phases);
618
- const result = await reader.read();
619
- addPhaseDuration(phases, streamWaitStartedAt, "streamWaitMs");
620
- if (result.done) {
621
- break;
691
+ const lifecycle = observeLambdaStreamFailure(stream, requestAbortController);
692
+ try {
693
+ while (true) {
694
+ const streamWaitStartedAt = phaseStartedAt(phases);
695
+ const result = await readLambdaBodyWithAbort(reader, lifecycle.signal);
696
+ addPhaseDuration(phases, streamWaitStartedAt, "streamWaitMs");
697
+ if (lifecycle.signal.aborted) {
698
+ throw lambdaAbortReason(lifecycle.signal);
699
+ }
700
+ if (result.done) {
701
+ break;
702
+ }
703
+ const streamWriteStartedAt = phaseStartedAt(phases);
704
+ await writeStreamingChunk(stream, result.value, lifecycle.signal);
705
+ addPhaseDuration(phases, streamWriteStartedAt, "streamWriteMs");
706
+ }
707
+ if (lifecycle.signal.aborted) {
708
+ throw lambdaAbortReason(lifecycle.signal);
709
+ }
710
+ stream.end();
711
+ }
712
+ catch (error) {
713
+ const reason = error instanceof Error ? error : new Error(String(error));
714
+ requestAbortController?.abort(reason);
715
+ try {
716
+ await reader.cancel(reason);
622
717
  }
623
- const streamWriteStartedAt = phaseStartedAt(phases);
624
- await writeStreamingChunk(stream, result.value);
625
- addPhaseDuration(phases, streamWriteStartedAt, "streamWriteMs");
718
+ catch {
719
+ // The response body may already be errored or cancelled.
720
+ }
721
+ if (stream.destroyed !== true) {
722
+ stream.destroy?.(reason);
723
+ }
724
+ }
725
+ finally {
726
+ lifecycle.dispose();
727
+ reader.releaseLock();
626
728
  }
627
- stream.end();
628
729
  }
629
730
  catch (error) {
630
731
  if (typeof stream.destroy === "function") {
@@ -634,6 +735,9 @@ async function streamResponseToLambda(response, responseStream, runtime, phases)
634
735
  throw error;
635
736
  }
636
737
  }
738
+ function isLambdaStreamingAborted(controller) {
739
+ return controller?.signal.aborted === true;
740
+ }
637
741
  function responseStreamingMetadata(response) {
638
742
  const headers = {};
639
743
  response.headers.forEach((value, key) => {
@@ -648,20 +752,116 @@ function responseStreamingMetadata(response) {
648
752
  statusCode: response.status,
649
753
  };
650
754
  }
651
- async function writeStreamingChunk(stream, chunk) {
652
- if (stream.write(chunk) !== false) {
755
+ async function writeStreamingChunk(stream, chunk, signal) {
756
+ if (signal.aborted) {
757
+ throw lambdaAbortReason(signal);
758
+ }
759
+ const accepted = stream.write(chunk);
760
+ if (signal.aborted) {
761
+ throw lambdaAbortReason(signal);
762
+ }
763
+ if (accepted !== false) {
653
764
  return;
654
765
  }
655
766
  if (typeof stream.once !== "function") {
656
767
  return;
657
768
  }
658
- await new Promise((resolve) => {
659
- stream.once?.("drain", resolve);
769
+ await new Promise((resolve, reject) => {
770
+ const dispose = () => {
771
+ removeLambdaStreamListener(stream, "drain", onDrain);
772
+ signal.removeEventListener("abort", onAbort);
773
+ };
774
+ const onDrain = () => {
775
+ dispose();
776
+ resolve();
777
+ };
778
+ const onAbort = () => {
779
+ dispose();
780
+ reject(lambdaAbortReason(signal));
781
+ };
782
+ addLambdaStreamListener(stream, "drain", onDrain);
783
+ signal.addEventListener("abort", onAbort, { once: true });
660
784
  });
661
785
  }
786
+ function observeLambdaStreamFailure(stream, abortController = new AbortController()) {
787
+ let disposed = false;
788
+ const dispose = () => {
789
+ if (disposed) {
790
+ return;
791
+ }
792
+ disposed = true;
793
+ removeLambdaStreamListener(stream, "close", onClose);
794
+ removeLambdaStreamListener(stream, "error", onError);
795
+ };
796
+ const abort = (reason) => {
797
+ abortController.abort(reason);
798
+ dispose();
799
+ };
800
+ const onClose = () => {
801
+ if (stream.writableEnded === true) {
802
+ dispose();
803
+ return;
804
+ }
805
+ abort(new Error("The AWS Lambda response stream closed during streaming."));
806
+ };
807
+ const onError = (error) => {
808
+ abort(error instanceof Error ? error : new Error(String(error ?? "AWS Lambda stream error")));
809
+ };
810
+ const canRemoveListeners = typeof stream.off === "function" || typeof stream.removeListener === "function";
811
+ if (canRemoveListeners) {
812
+ addLambdaStreamListener(stream, "close", onClose);
813
+ addLambdaStreamListener(stream, "error", onError);
814
+ }
815
+ if (abortController.signal.aborted) {
816
+ dispose();
817
+ }
818
+ else if (stream.destroyed === true && stream.writableEnded !== true) {
819
+ onClose();
820
+ }
821
+ return { dispose, signal: abortController.signal };
822
+ }
823
+ function readLambdaBodyWithAbort(reader, signal) {
824
+ if (signal.aborted) {
825
+ return Promise.reject(lambdaAbortReason(signal));
826
+ }
827
+ return new Promise((resolve, reject) => {
828
+ const onAbort = () => {
829
+ signal.removeEventListener("abort", onAbort);
830
+ reject(lambdaAbortReason(signal));
831
+ };
832
+ signal.addEventListener("abort", onAbort, { once: true });
833
+ void reader.read().then((result) => {
834
+ signal.removeEventListener("abort", onAbort);
835
+ resolve(result);
836
+ }, (error) => {
837
+ signal.removeEventListener("abort", onAbort);
838
+ reject(error);
839
+ });
840
+ });
841
+ }
842
+ function removeLambdaStreamListener(stream, event, listener) {
843
+ if (typeof stream.off === "function") {
844
+ stream.off(event, listener);
845
+ return;
846
+ }
847
+ stream.removeListener?.(event, listener);
848
+ }
849
+ function addLambdaStreamListener(stream, event, listener) {
850
+ const once = stream.once;
851
+ try {
852
+ once?.call(stream, event, listener);
853
+ }
854
+ catch {
855
+ // A structurally compatible drain-only stream may reject lifecycle event names.
856
+ }
857
+ }
858
+ function lambdaAbortReason(signal) {
859
+ return signal.reason instanceof Error
860
+ ? signal.reason
861
+ : new Error("AWS Lambda response streaming was aborted.");
862
+ }
662
863
  function awsLambdaRuntime() {
663
- const runtime = globalThis
664
- .awslambda;
864
+ const runtime = globalThis.awslambda;
665
865
  if (typeof runtime?.streamifyResponse !== "function" ||
666
866
  typeof runtime.HttpResponseStream?.from !== "function") {
667
867
  throw new Error("AWS Lambda response streaming requires the Node.js Lambda runtime awslambda.streamifyResponse() and awslambda.HttpResponseStream.from().");
@@ -704,18 +904,56 @@ function splitSetCookieHeader(header) {
704
904
  cookies.push(header.slice(start).trim());
705
905
  return cookies.filter((cookie) => cookie.length > 0);
706
906
  }
707
- function isTextContentType(contentType) {
907
+ function isUtf8TextContentType(contentType) {
708
908
  if (contentType === null) {
709
- return true;
909
+ return false;
710
910
  }
711
- const type = contentType.toLowerCase().split(";")[0]?.trim() ?? "";
712
- return (type.startsWith("text/") ||
911
+ const [rawType = "", ...parameters] = contentType.split(";");
912
+ const type = rawType.trim().toLowerCase();
913
+ const textual = type.startsWith("text/") ||
713
914
  type === "application/json" ||
714
915
  type === "application/javascript" ||
715
916
  type === "application/x-javascript" ||
716
917
  type === "application/xml" ||
717
918
  type === "application/x-www-form-urlencoded" ||
718
919
  type.endsWith("+json") ||
719
- type.endsWith("+xml"));
920
+ type.endsWith("+xml");
921
+ if (!textual) {
922
+ return false;
923
+ }
924
+ let hasCharset = false;
925
+ for (const parameter of parameters) {
926
+ const equals = parameter.indexOf("=");
927
+ const name = (equals === -1 ? parameter : parameter.slice(0, equals)).trim().toLowerCase();
928
+ if (name !== "charset") {
929
+ continue;
930
+ }
931
+ hasCharset = true;
932
+ if (equals === -1) {
933
+ return false;
934
+ }
935
+ const value = unquoteContentTypeParameter(parameter.slice(equals + 1).trim()).toLowerCase();
936
+ if (value !== "utf-8" && value !== "utf8") {
937
+ return false;
938
+ }
939
+ }
940
+ if (hasCharset) {
941
+ return true;
942
+ }
943
+ return (type === "application/json" ||
944
+ type === "application/javascript" ||
945
+ type === "application/x-javascript" ||
946
+ type === "text/javascript" ||
947
+ type.endsWith("+json"));
948
+ }
949
+ function unquoteContentTypeParameter(value) {
950
+ if (value.length < 2) {
951
+ return value;
952
+ }
953
+ const first = value[0];
954
+ const last = value.at(-1);
955
+ return (first === '"' && last === '"') || (first === "'" && last === "'")
956
+ ? value.slice(1, -1)
957
+ : value;
720
958
  }
721
959
  //# sourceMappingURL=aws-lambda.js.map