@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,8 +1,9 @@
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
+ import type { DehydrateOptions } from "@reckona/mreact-query";
6
7
  import type { AppRouterServerActionOptions } from "../actions.js";
7
8
  import type { AppRouterCache } from "../cache.js";
8
9
  import type { AppRouterImportPolicy } from "../import-policy.js";
@@ -25,10 +26,7 @@ import type { RouterInstrumentation } from "../trace.js";
25
26
  /** Re-exports cache contracts used by AWS Lambda handlers. */
26
27
  export type { AppRouterCache, AppRouterCacheEntry } from "../cache.js";
27
28
  /** Re-exports server action contracts used by AWS Lambda handlers. */
28
- export type {
29
- AppRouterAllowedServerAction,
30
- AppRouterServerActionOptions,
31
- } from "../actions.js";
29
+ export type { AppRouterAllowedServerAction, AppRouterServerActionOptions } from "../actions.js";
32
30
  /** Re-exports logger contracts used by AWS Lambda handlers. */
33
31
  export type {
34
32
  AppRouterCspInlineNonceWarningLogEvent,
@@ -41,6 +39,8 @@ export type {
41
39
  AppRouterRequestStartLogEvent,
42
40
  AppRouterRequestTimingLogEvent,
43
41
  AppRouterRuntime,
42
+ AppRouterUpgradeErrorLogEvent,
43
+ AppRouterUpgradeRejectedLogEvent,
44
44
  } from "../logger.js";
45
45
  /** Re-exports response hook contracts used by AWS Lambda handlers. */
46
46
  export type { AppRouterResponseHook, AppRouterResponseHookContext } from "../render.js";
@@ -63,18 +63,15 @@ import {
63
63
  createBuiltRequestRuntime,
64
64
  preloadBuiltAppRuntime,
65
65
  resolveRequestHost,
66
- warnIfImplicitHostTrust,
67
66
  type AppRouterPrerenderStore,
68
67
  type RequestHostPolicy,
69
68
  type ResponseSinkStrategy,
70
69
  } from "../serve.js";
71
70
 
71
+ let warnedImplicitAwsLambdaHostPolicy = false;
72
+
72
73
  /** Re-exports request and rendering contracts used by AWS Lambda handlers. */
73
- export type {
74
- AppRouterPrerenderStore,
75
- RequestHostPolicy,
76
- ResponseSinkStrategy,
77
- } from "../serve.js";
74
+ export type { AppRouterPrerenderStore, RequestHostPolicy, ResponseSinkStrategy } from "../serve.js";
78
75
 
79
76
  /**
80
77
  * Represents the AWS API Gateway HTTP API v2 event consumed by buffered handlers.
@@ -120,9 +117,25 @@ export interface AwsLambdaStreamingResponseMetadata {
120
117
  */
121
118
  export interface AwsLambdaStreamingResponseStream {
122
119
  destroy?: ((error?: unknown) => void) | undefined;
120
+ /** Reports whether the runtime output stream has already been destroyed. */
121
+ destroyed?: boolean | undefined;
123
122
  end(): void;
124
- once?: ((event: "drain", listener: () => void) => unknown) | undefined;
123
+ /** Removes a lifecycle listener when the runtime stream implements Node EventEmitter semantics. */
124
+ off?:
125
+ | ((event: "close" | "drain" | "error", listener: (error?: unknown) => void) => unknown)
126
+ | undefined;
127
+ /** Registers drain-only or full Node-compatible lifecycle listeners. */
128
+ once?:
129
+ | ((event: "drain", listener: () => void) => unknown)
130
+ | ((event: "close" | "drain" | "error", listener: (error?: unknown) => void) => unknown)
131
+ | undefined;
132
+ /** Removes a lifecycle listener on runtimes that expose the legacy EventEmitter API. */
133
+ removeListener?:
134
+ | ((event: "close" | "drain" | "error", listener: (error?: unknown) => void) => unknown)
135
+ | undefined;
125
136
  write(chunk: string | Uint8Array): boolean;
137
+ /** Reports whether the runtime output stream ended normally. */
138
+ writableEnded?: boolean | undefined;
126
139
  }
127
140
 
128
141
  /**
@@ -130,6 +143,7 @@ export interface AwsLambdaStreamingResponseStream {
130
143
  */
131
144
  export interface AwsLambdaRequestHandlerOptions {
132
145
  allowedHosts?: readonly string[] | undefined;
146
+ dehydrateOptions?: DehydrateOptions | undefined;
133
147
  errorHandler?:
134
148
  | ((error: unknown) => {
135
149
  body: string;
@@ -157,10 +171,7 @@ export interface AwsLambdaRequestHandlerOptions {
157
171
  /**
158
172
  * Selects how an AWS Lambda handler resolves its app-router import policy.
159
173
  */
160
- export type AwsLambdaImportPolicy =
161
- | AppRouterImportPolicy
162
- | "generated"
163
- | { fromManifest: true };
174
+ export type AwsLambdaImportPolicy = AppRouterImportPolicy | "generated" | { fromManifest: true };
164
175
 
165
176
  /**
166
177
  * Configures preload behavior for built app-router modules in AWS Lambda.
@@ -197,7 +208,7 @@ export type AwsLambdaStreamingRequestHandler<TContext = unknown> = (
197
208
  ) => Promise<void>;
198
209
 
199
210
  const invalidAwsLambdaHttpEventV2Diagnostic =
200
- 'Expected an AWS Lambda HTTP API payload format 2.0 event with rawPath and requestContext.http.method.';
211
+ "Expected an AWS Lambda HTTP API payload format 2.0 event with rawPath and requestContext.http.method.";
201
212
 
202
213
  function invalidAwsLambdaHttpEventV2Result(): AwsLambdaHttpResultV2 {
203
214
  return {
@@ -223,7 +234,7 @@ function invalidAwsLambdaHttpEventV2Response(): Response {
223
234
  export function createAwsLambdaRequestHandler(
224
235
  options: AwsLambdaRequestHandlerOptions,
225
236
  ): AwsLambdaRequestHandler {
226
- warnIfImplicitHostTrust(options);
237
+ warnIfImplicitAwsLambdaHostPolicy(options);
227
238
  let handler: AwsLambdaRequestHandler | undefined;
228
239
 
229
240
  return async (event) => {
@@ -235,7 +246,11 @@ export function createAwsLambdaRequestHandler(
235
246
 
236
247
  handler ??= (() => {
237
248
  const runtimeDirPromise = prepareAwsLambdaRuntimeDir(options);
238
- const runtimePreloadPromise = startAwsLambdaRuntimePreload(options, runtimeDirPromise, "middleware");
249
+ const runtimePreloadPromise = startAwsLambdaRuntimePreload(
250
+ options,
251
+ runtimeDirPromise,
252
+ "middleware",
253
+ );
239
254
  void runtimePreloadPromise?.catch(() => {});
240
255
  return createAwsLambdaRequestHandlerFromRuntime(
241
256
  options,
@@ -257,7 +272,7 @@ export function createAwsLambdaRequestHandler(
257
272
  export async function createPreloadedAwsLambdaRequestHandler(
258
273
  options: AwsLambdaRequestHandlerOptions,
259
274
  ): Promise<AwsLambdaRequestHandler> {
260
- warnIfImplicitHostTrust(options);
275
+ warnIfImplicitAwsLambdaHostPolicy(options);
261
276
  let handler: AwsLambdaRequestHandler | undefined;
262
277
 
263
278
  return async (event) => {
@@ -287,10 +302,8 @@ export async function createPreloadedAwsLambdaRequestHandler(
287
302
  *
288
303
  * 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.
289
304
  */
290
- export async function warmAwsLambdaRuntime(
291
- options: AwsLambdaRequestHandlerOptions,
292
- ): Promise<void> {
293
- warnIfImplicitHostTrust(options);
305
+ export async function warmAwsLambdaRuntime(options: AwsLambdaRequestHandlerOptions): Promise<void> {
306
+ warnIfImplicitAwsLambdaHostPolicy(options);
294
307
  const runtimeDir = await prepareAwsLambdaRuntimeDir(options);
295
308
  await preloadAwsLambdaRuntime(options, runtimeDir);
296
309
  }
@@ -310,16 +323,18 @@ function createAwsLambdaRequestHandlerFromRuntime(
310
323
 
311
324
  const startedAt = logNow();
312
325
  const phases = createAwsLambdaTimingPhases(options);
313
- const eventToRequestStartedAt = phaseStartedAt(phases);
314
- const request = eventToRequest(event, options);
315
- finishPhase(phases, eventToRequestStartedAt, "eventToRequestMs");
316
- const logFields = requestLogFields(request, "aws-lambda");
317
- emitRouterLog(options.logger, "info", {
318
- ...logFields,
319
- type: "router:request:start",
320
- });
326
+ let request: Request | undefined;
327
+ let logFields = awsLambdaEventLogFields(event);
321
328
 
322
329
  try {
330
+ const eventToRequestStartedAt = phaseStartedAt(phases);
331
+ request = eventToRequest(event, options);
332
+ finishPhase(phases, eventToRequestStartedAt, "eventToRequestMs");
333
+ logFields = requestLogFields(request, "aws-lambda");
334
+ emitRouterLog(options.logger, "info", {
335
+ ...logFields,
336
+ type: "router:request:start",
337
+ });
323
338
  const runtimeDirStartedAt = phaseStartedAt(phases);
324
339
  const runtimeDir = await runtimeDirPromise;
325
340
  finishPhase(phases, runtimeDirStartedAt, "runtimeDirMs");
@@ -337,12 +352,9 @@ function createAwsLambdaRequestHandlerFromRuntime(
337
352
  outDir: options.outDir,
338
353
  runtimeDir,
339
354
  });
340
- const preload = awsLambdaRenderPreload(
341
- options,
342
- runtimePreloadPromise,
343
- defaultPreloadMode,
344
- );
355
+ const preload = awsLambdaRenderPreload(options, runtimePreloadPromise, defaultPreloadMode);
345
356
  const response = await builtRuntime.render(request, {
357
+ dehydrateOptions: options.dehydrateOptions,
346
358
  instrumentation: options.instrumentation,
347
359
  logger: awsLambdaRenderLogger(options),
348
360
  onResponse: options.onResponse,
@@ -401,7 +413,7 @@ function createAwsLambdaRequestHandlerFromRuntime(
401
413
  export function createAwsLambdaStreamingRequestHandler<TContext = unknown>(
402
414
  options: AwsLambdaRequestHandlerOptions,
403
415
  ): AwsLambdaStreamingRequestHandler<TContext> {
404
- warnIfImplicitHostTrust(options);
416
+ warnIfImplicitAwsLambdaHostPolicy(options);
405
417
  const runtime = awsLambdaRuntime();
406
418
  let handler: AwsLambdaStreamingRequestHandler<TContext> | undefined;
407
419
 
@@ -409,17 +421,17 @@ export function createAwsLambdaStreamingRequestHandler<TContext = unknown>(
409
421
  try {
410
422
  validateAwsLambdaHttpEventV2(event);
411
423
  } catch {
412
- await streamResponseToLambda(
413
- invalidAwsLambdaHttpEventV2Response(),
414
- responseStream,
415
- runtime,
416
- );
424
+ await streamResponseToLambda(invalidAwsLambdaHttpEventV2Response(), responseStream, runtime);
417
425
  return;
418
426
  }
419
427
 
420
428
  handler ??= (() => {
421
429
  const runtimeDirPromise = prepareAwsLambdaRuntimeDir(options);
422
- const runtimePreloadPromise = startAwsLambdaRuntimePreload(options, runtimeDirPromise, "middleware");
430
+ const runtimePreloadPromise = startAwsLambdaRuntimePreload(
431
+ options,
432
+ runtimeDirPromise,
433
+ "middleware",
434
+ );
423
435
  void runtimePreloadPromise?.catch(() => {});
424
436
  return createAwsLambdaStreamingRequestHandlerFromRuntime(
425
437
  options,
@@ -442,7 +454,7 @@ export function createAwsLambdaStreamingRequestHandler<TContext = unknown>(
442
454
  export async function createPreloadedAwsLambdaStreamingRequestHandler<TContext = unknown>(
443
455
  options: AwsLambdaRequestHandlerOptions,
444
456
  ): Promise<AwsLambdaStreamingRequestHandler<TContext>> {
445
- warnIfImplicitHostTrust(options);
457
+ warnIfImplicitAwsLambdaHostPolicy(options);
446
458
  const runtime = awsLambdaRuntime();
447
459
  let handler: AwsLambdaStreamingRequestHandler<TContext> | undefined;
448
460
 
@@ -450,11 +462,7 @@ export async function createPreloadedAwsLambdaStreamingRequestHandler<TContext =
450
462
  try {
451
463
  validateAwsLambdaHttpEventV2(event);
452
464
  } catch {
453
- await streamResponseToLambda(
454
- invalidAwsLambdaHttpEventV2Response(),
455
- responseStream,
456
- runtime,
457
- );
465
+ await streamResponseToLambda(invalidAwsLambdaHttpEventV2Response(), responseStream, runtime);
458
466
  return;
459
467
  }
460
468
 
@@ -596,8 +604,12 @@ async function readGeneratedAwsLambdaImportPolicy(outDir: string): Promise<AppRo
596
604
  return await promise;
597
605
  }
598
606
 
599
- async function readGeneratedAwsLambdaImportPolicyInner(outDir: string): Promise<AppRouterImportPolicy> {
600
- const policy = JSON.parse(await readFile(join(outDir, "server", "import-policy.json"), "utf8")) as {
607
+ async function readGeneratedAwsLambdaImportPolicyInner(
608
+ outDir: string,
609
+ ): Promise<AppRouterImportPolicy> {
610
+ const policy = JSON.parse(
611
+ await readFile(join(outDir, "server", "import-policy.json"), "utf8"),
612
+ ) as {
601
613
  runtimePackages?: readonly string[] | undefined;
602
614
  };
603
615
 
@@ -624,26 +636,27 @@ function createAwsLambdaStreamingRequestHandlerFromRuntime<TContext = unknown>(
624
636
  try {
625
637
  validateAwsLambdaHttpEventV2(event);
626
638
  } catch {
627
- await streamResponseToLambda(
628
- invalidAwsLambdaHttpEventV2Response(),
629
- responseStream,
630
- runtime,
631
- );
639
+ await streamResponseToLambda(invalidAwsLambdaHttpEventV2Response(), responseStream, runtime);
632
640
  return;
633
641
  }
634
642
 
635
643
  const startedAt = logNow();
636
644
  const phases = createAwsLambdaTimingPhases(options);
637
- const eventToRequestStartedAt = phaseStartedAt(phases);
638
- const request = eventToRequest(event, options);
639
- finishPhase(phases, eventToRequestStartedAt, "eventToRequestMs");
640
- const logFields = requestLogFields(request, "aws-lambda");
641
- emitRouterLog(options.logger, "info", {
642
- ...logFields,
643
- type: "router:request:start",
644
- });
645
+ const requestAbortController = new AbortController();
646
+ let request: Request | undefined;
647
+ let requestLifecycle: ReturnType<typeof observeLambdaStreamFailure> | undefined;
648
+ let logFields = awsLambdaEventLogFields(event);
645
649
 
646
650
  try {
651
+ const eventToRequestStartedAt = phaseStartedAt(phases);
652
+ request = eventToRequest(event, options, requestAbortController.signal);
653
+ requestLifecycle = observeLambdaStreamFailure(responseStream, requestAbortController);
654
+ finishPhase(phases, eventToRequestStartedAt, "eventToRequestMs");
655
+ logFields = requestLogFields(request, "aws-lambda");
656
+ emitRouterLog(options.logger, "info", {
657
+ ...logFields,
658
+ type: "router:request:start",
659
+ });
647
660
  const runtimeDirStartedAt = phaseStartedAt(phases);
648
661
  const runtimeDir = await runtimeDirPromise;
649
662
  finishPhase(phases, runtimeDirStartedAt, "runtimeDirMs");
@@ -661,12 +674,9 @@ function createAwsLambdaStreamingRequestHandlerFromRuntime<TContext = unknown>(
661
674
  outDir: options.outDir,
662
675
  runtimeDir,
663
676
  });
664
- const preload = awsLambdaRenderPreload(
665
- options,
666
- runtimePreloadPromise,
667
- defaultPreloadMode,
668
- );
677
+ const preload = awsLambdaRenderPreload(options, runtimePreloadPromise, defaultPreloadMode);
669
678
  const response = await builtRuntime.render(request, {
679
+ dehydrateOptions: options.dehydrateOptions,
670
680
  instrumentation: options.instrumentation,
671
681
  logger: awsLambdaRenderLogger(options),
672
682
  onResponse: options.onResponse,
@@ -678,7 +688,13 @@ function createAwsLambdaStreamingRequestHandlerFromRuntime<TContext = unknown>(
678
688
  });
679
689
  finishPhase(phases, renderStartedAt, "renderMs");
680
690
  const responseStreamingStartedAt = phaseStartedAt(phases);
681
- await streamResponseToLambda(response, responseStream, runtime, phases);
691
+ await streamResponseToLambda(
692
+ response,
693
+ responseStream,
694
+ runtime,
695
+ phases,
696
+ requestAbortController,
697
+ );
682
698
  finishPhase(phases, responseStreamingStartedAt, "responseStreamingMs");
683
699
  emitRouterLog(options.logger, "info", {
684
700
  ...logFields,
@@ -695,6 +711,10 @@ function createAwsLambdaStreamingRequestHandlerFromRuntime<TContext = unknown>(
695
711
  type: "router:request:error",
696
712
  });
697
713
 
714
+ if (requestAbortController.signal.aborted) {
715
+ return;
716
+ }
717
+
698
718
  const payload = options.errorHandler
699
719
  ? options.errorHandler(error)
700
720
  : { body: "Internal Server Error", status: 500 };
@@ -710,7 +730,15 @@ function createAwsLambdaStreamingRequestHandlerFromRuntime<TContext = unknown>(
710
730
  request,
711
731
  );
712
732
 
713
- await streamResponseToLambda(response, responseStream, runtime, phases);
733
+ await streamResponseToLambda(
734
+ response,
735
+ responseStream,
736
+ runtime,
737
+ phases,
738
+ requestAbortController,
739
+ );
740
+ } finally {
741
+ requestLifecycle?.dispose();
714
742
  }
715
743
  });
716
744
  }
@@ -718,8 +746,12 @@ function createAwsLambdaStreamingRequestHandlerFromRuntime<TContext = unknown>(
718
746
  async function applyAwsLambdaResponseHook(
719
747
  response: Response,
720
748
  options: Pick<AwsLambdaRequestHandlerOptions, "onResponse">,
721
- request: Request,
749
+ request: Request | undefined,
722
750
  ): Promise<Response> {
751
+ if (request === undefined) {
752
+ return response;
753
+ }
754
+
723
755
  const hooked = await options.onResponse?.(response, { request });
724
756
 
725
757
  return hooked instanceof Response ? hooked : response;
@@ -798,6 +830,7 @@ function isNodeErrorCode(error: unknown, code: string): boolean {
798
830
  function eventToRequest(
799
831
  event: AwsLambdaHttpEventV2,
800
832
  options: AwsLambdaRequestHandlerOptions,
833
+ signal?: AbortSignal,
801
834
  ): Request {
802
835
  validateAwsLambdaHttpEventV2(event);
803
836
  const headers = eventHeaders(event);
@@ -808,21 +841,67 @@ function eventToRequest(
808
841
  hostPolicy: lambdaHostPolicy(options),
809
842
  rawHost: rawHost ?? undefined,
810
843
  });
811
- const protocol = lambdaRequestProtocol(event, headers, options);
812
- const rawPath = event.rawPath;
813
- const rawQueryString = event.rawQueryString === "" ? "" : `?${event.rawQueryString}`;
844
+ const protocol = lambdaRequestProtocol(headers, options);
814
845
  const method = event.requestContext.http.method;
815
846
  const init: RequestInit = {
816
847
  headers,
817
848
  method,
849
+ ...(signal === undefined ? {} : { signal }),
818
850
  };
819
851
 
820
852
  if (method !== "GET" && method !== "HEAD" && event.body !== undefined) {
821
- init.body =
822
- event.isBase64Encoded === true ? Buffer.from(event.body, "base64") : event.body;
853
+ init.body = event.isBase64Encoded === true ? Buffer.from(event.body, "base64") : event.body;
854
+ }
855
+
856
+ return new Request(lambdaRequestUrl(protocol, host, event.rawPath, event.rawQueryString), init);
857
+ }
858
+
859
+ function lambdaRequestUrl(
860
+ protocol: "http" | "https",
861
+ host: string,
862
+ rawPath: string,
863
+ rawQueryString: string,
864
+ ): URL {
865
+ if (hasInvalidLambdaRequestAuthorityCharacter(host)) {
866
+ throw new TypeError("AWS Lambda request Host is not a valid URL authority");
867
+ }
868
+
869
+ const url = new URL(`${protocol}://${host}`);
870
+ if (
871
+ url.protocol !== `${protocol}:` ||
872
+ url.host === "" ||
873
+ url.username !== "" ||
874
+ url.password !== "" ||
875
+ url.pathname !== "/" ||
876
+ url.search !== "" ||
877
+ url.hash !== ""
878
+ ) {
879
+ throw new TypeError("AWS Lambda request Host is not a valid URL authority");
880
+ }
881
+
882
+ url.pathname = rawPath;
883
+ url.search = rawQueryString;
884
+ return url;
885
+ }
886
+
887
+ function hasInvalidLambdaRequestAuthorityCharacter(host: string): boolean {
888
+ for (const character of host) {
889
+ const codePoint = character.codePointAt(0) ?? 0;
890
+ if (
891
+ character === "/" ||
892
+ character === "?" ||
893
+ character === "#" ||
894
+ character === "\\" ||
895
+ character === "@" ||
896
+ character.trim() === "" ||
897
+ codePoint < 0x20 ||
898
+ codePoint === 0x7f
899
+ ) {
900
+ return true;
901
+ }
823
902
  }
824
903
 
825
- return new Request(`${protocol}://${host}${rawPath}${rawQueryString}`, init);
904
+ return false;
826
905
  }
827
906
 
828
907
  function validateAwsLambdaHttpEventV2(event: AwsLambdaHttpEventV2): void {
@@ -837,10 +916,12 @@ function validateAwsLambdaHttpEventV2(event: AwsLambdaHttpEventV2): void {
837
916
  typeof (candidate as { rawQueryString?: unknown }).rawQueryString !== "string" ||
838
917
  typeof (candidate as { requestContext?: unknown }).requestContext !== "object" ||
839
918
  (candidate as { requestContext?: unknown }).requestContext === null ||
840
- typeof ((candidate as { requestContext: { http?: unknown } }).requestContext.http) !== "object" ||
919
+ typeof (candidate as { requestContext: { http?: unknown } }).requestContext.http !== "object" ||
841
920
  (candidate as { requestContext: { http?: unknown } }).requestContext.http === null ||
842
- typeof ((candidate as { requestContext: { http: { method?: unknown } } }).requestContext.http.method) !== "string" ||
843
- (candidate as { requestContext: { http: { method: string } } }).requestContext.http.method === ""
921
+ typeof (candidate as { requestContext: { http: { method?: unknown } } }).requestContext.http
922
+ .method !== "string" ||
923
+ (candidate as { requestContext: { http: { method: string } } }).requestContext.http.method ===
924
+ ""
844
925
  ) {
845
926
  throw new Error(invalidAwsLambdaHttpEventV2Diagnostic);
846
927
  }
@@ -857,14 +938,27 @@ function lambdaRequestHost(
857
938
  return headers.get("host");
858
939
  }
859
940
 
860
- function lambdaHostPolicy(
861
- options: AwsLambdaRequestHandlerOptions,
862
- ): RequestHostPolicy | undefined {
941
+ function lambdaHostPolicy(options: AwsLambdaRequestHandlerOptions): RequestHostPolicy | undefined {
863
942
  return options.hostPolicy ?? (process.env.NODE_ENV === "production" ? "strict" : undefined);
864
943
  }
865
944
 
945
+ function warnIfImplicitAwsLambdaHostPolicy(options: AwsLambdaRequestHandlerOptions): void {
946
+ if (
947
+ process.env.NODE_ENV !== "production" ||
948
+ options.allowedHosts !== undefined ||
949
+ options.hostPolicy !== undefined ||
950
+ warnedImplicitAwsLambdaHostPolicy
951
+ ) {
952
+ return;
953
+ }
954
+
955
+ warnedImplicitAwsLambdaHostPolicy = true;
956
+ console.error(
957
+ '[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.',
958
+ );
959
+ }
960
+
866
961
  function lambdaRequestProtocol(
867
- event: AwsLambdaHttpEventV2,
868
962
  headers: Headers,
869
963
  options: AwsLambdaRequestHandlerOptions,
870
964
  ): "http" | "https" {
@@ -872,7 +966,7 @@ function lambdaRequestProtocol(
872
966
  return normalizeRequestProtocol(firstForwardedValue(headers.get("x-forwarded-proto")));
873
967
  }
874
968
 
875
- return normalizeRequestProtocol(event.requestContext?.http?.protocol);
969
+ return "https";
876
970
  }
877
971
 
878
972
  function normalizeRequestProtocol(value: string | undefined): "http" | "https" {
@@ -880,8 +974,15 @@ function normalizeRequestProtocol(value: string | undefined): "http" | "https" {
880
974
  return "https";
881
975
  }
882
976
 
883
- const normalized = value.toLowerCase();
884
- return normalized === "http" || normalized.startsWith("http/") ? "http" : "https";
977
+ return value.toLowerCase() === "http" ? "http" : "https";
978
+ }
979
+
980
+ function awsLambdaEventLogFields(event: AwsLambdaHttpEventV2): RouterRequestLogFields {
981
+ return {
982
+ method: event.requestContext.http.method,
983
+ path: event.rawPath,
984
+ runtime: "aws-lambda",
985
+ };
885
986
  }
886
987
 
887
988
  function eventHeaders(event: AwsLambdaHttpEventV2): Headers {
@@ -929,7 +1030,7 @@ async function responseToLambdaResult(
929
1030
 
930
1031
  const bodyEncodeStartedAt = phaseStartedAt(phases);
931
1032
  const contentType = response.headers.get("content-type");
932
- const text = isTextContentType(contentType);
1033
+ const text = isUtf8TextContentType(contentType) && isUtf8(bytes);
933
1034
  const result = {
934
1035
  body: text ? bytes.toString("utf8") : bytes.toString("base64"),
935
1036
  ...(cookies.length === 0 ? {} : { cookies }),
@@ -1055,12 +1156,21 @@ async function streamResponseToLambda(
1055
1156
  responseStream: AwsLambdaStreamingResponseStream,
1056
1157
  runtime: AwsLambdaRuntime,
1057
1158
  phases?: Record<string, number> | undefined,
1159
+ requestAbortController?: AbortController,
1058
1160
  ): Promise<void> {
1161
+ if (isLambdaStreamingAborted(requestAbortController)) {
1162
+ return;
1163
+ }
1164
+
1059
1165
  const stream = runtime.HttpResponseStream.from(
1060
1166
  responseStream,
1061
1167
  responseStreamingMetadata(response),
1062
1168
  );
1063
1169
 
1170
+ if (isLambdaStreamingAborted(requestAbortController)) {
1171
+ return;
1172
+ }
1173
+
1064
1174
  try {
1065
1175
  if (response.body === null) {
1066
1176
  stream.end();
@@ -1068,22 +1178,46 @@ async function streamResponseToLambda(
1068
1178
  }
1069
1179
 
1070
1180
  const reader = response.body.getReader();
1181
+ const lifecycle = observeLambdaStreamFailure(stream, requestAbortController);
1071
1182
 
1072
- while (true) {
1073
- const streamWaitStartedAt = phaseStartedAt(phases);
1074
- const result = await reader.read();
1075
- addPhaseDuration(phases, streamWaitStartedAt, "streamWaitMs");
1076
-
1077
- if (result.done) {
1078
- break;
1183
+ try {
1184
+ while (true) {
1185
+ const streamWaitStartedAt = phaseStartedAt(phases);
1186
+ const result = await readLambdaBodyWithAbort(reader, lifecycle.signal);
1187
+ addPhaseDuration(phases, streamWaitStartedAt, "streamWaitMs");
1188
+
1189
+ if (lifecycle.signal.aborted) {
1190
+ throw lambdaAbortReason(lifecycle.signal);
1191
+ }
1192
+
1193
+ if (result.done) {
1194
+ break;
1195
+ }
1196
+
1197
+ const streamWriteStartedAt = phaseStartedAt(phases);
1198
+ await writeStreamingChunk(stream, result.value, lifecycle.signal);
1199
+ addPhaseDuration(phases, streamWriteStartedAt, "streamWriteMs");
1079
1200
  }
1080
1201
 
1081
- const streamWriteStartedAt = phaseStartedAt(phases);
1082
- await writeStreamingChunk(stream, result.value);
1083
- addPhaseDuration(phases, streamWriteStartedAt, "streamWriteMs");
1202
+ if (lifecycle.signal.aborted) {
1203
+ throw lambdaAbortReason(lifecycle.signal);
1204
+ }
1205
+ stream.end();
1206
+ } catch (error) {
1207
+ const reason = error instanceof Error ? error : new Error(String(error));
1208
+ requestAbortController?.abort(reason);
1209
+ try {
1210
+ await reader.cancel(reason);
1211
+ } catch {
1212
+ // The response body may already be errored or cancelled.
1213
+ }
1214
+ if (stream.destroyed !== true) {
1215
+ stream.destroy?.(reason);
1216
+ }
1217
+ } finally {
1218
+ lifecycle.dispose();
1219
+ reader.releaseLock();
1084
1220
  }
1085
-
1086
- stream.end();
1087
1221
  } catch (error) {
1088
1222
  if (typeof stream.destroy === "function") {
1089
1223
  stream.destroy(error);
@@ -1094,9 +1228,11 @@ async function streamResponseToLambda(
1094
1228
  }
1095
1229
  }
1096
1230
 
1097
- function responseStreamingMetadata(
1098
- response: Response,
1099
- ): AwsLambdaStreamingResponseMetadata {
1231
+ function isLambdaStreamingAborted(controller: AbortController | undefined): boolean {
1232
+ return controller?.signal.aborted === true;
1233
+ }
1234
+
1235
+ function responseStreamingMetadata(response: Response): AwsLambdaStreamingResponseMetadata {
1100
1236
  const headers: Record<string, string> = {};
1101
1237
  response.headers.forEach((value, key) => {
1102
1238
  if (key !== "set-cookie") {
@@ -1116,8 +1252,18 @@ function responseStreamingMetadata(
1116
1252
  async function writeStreamingChunk(
1117
1253
  stream: AwsLambdaStreamingResponseStream,
1118
1254
  chunk: Uint8Array,
1255
+ signal: AbortSignal,
1119
1256
  ): Promise<void> {
1120
- if (stream.write(chunk) !== false) {
1257
+ if (signal.aborted) {
1258
+ throw lambdaAbortReason(signal);
1259
+ }
1260
+
1261
+ const accepted = stream.write(chunk);
1262
+ if (signal.aborted) {
1263
+ throw lambdaAbortReason(signal);
1264
+ }
1265
+
1266
+ if (accepted !== false) {
1121
1267
  return;
1122
1268
  }
1123
1269
 
@@ -1125,11 +1271,131 @@ async function writeStreamingChunk(
1125
1271
  return;
1126
1272
  }
1127
1273
 
1128
- await new Promise<void>((resolve) => {
1129
- stream.once?.("drain", resolve);
1274
+ await new Promise<void>((resolve, reject) => {
1275
+ const dispose = (): void => {
1276
+ removeLambdaStreamListener(stream, "drain", onDrain);
1277
+ signal.removeEventListener("abort", onAbort);
1278
+ };
1279
+ const onDrain = (): void => {
1280
+ dispose();
1281
+ resolve();
1282
+ };
1283
+ const onAbort = (): void => {
1284
+ dispose();
1285
+ reject(lambdaAbortReason(signal));
1286
+ };
1287
+ addLambdaStreamListener(stream, "drain", onDrain);
1288
+ signal.addEventListener("abort", onAbort, { once: true });
1289
+ });
1290
+ }
1291
+
1292
+ function observeLambdaStreamFailure(
1293
+ stream: AwsLambdaStreamingResponseStream,
1294
+ abortController: AbortController = new AbortController(),
1295
+ ): {
1296
+ dispose(): void;
1297
+ signal: AbortSignal;
1298
+ } {
1299
+ let disposed = false;
1300
+
1301
+ const dispose = (): void => {
1302
+ if (disposed) {
1303
+ return;
1304
+ }
1305
+ disposed = true;
1306
+ removeLambdaStreamListener(stream, "close", onClose);
1307
+ removeLambdaStreamListener(stream, "error", onError);
1308
+ };
1309
+ const abort = (reason: Error): void => {
1310
+ abortController.abort(reason);
1311
+ dispose();
1312
+ };
1313
+ const onClose = (): void => {
1314
+ if (stream.writableEnded === true) {
1315
+ dispose();
1316
+ return;
1317
+ }
1318
+ abort(new Error("The AWS Lambda response stream closed during streaming."));
1319
+ };
1320
+ const onError = (error?: unknown): void => {
1321
+ abort(error instanceof Error ? error : new Error(String(error ?? "AWS Lambda stream error")));
1322
+ };
1323
+
1324
+ const canRemoveListeners =
1325
+ typeof stream.off === "function" || typeof stream.removeListener === "function";
1326
+ if (canRemoveListeners) {
1327
+ addLambdaStreamListener(stream, "close", onClose);
1328
+ addLambdaStreamListener(stream, "error", onError);
1329
+ }
1330
+ if (abortController.signal.aborted) {
1331
+ dispose();
1332
+ } else if (stream.destroyed === true && stream.writableEnded !== true) {
1333
+ onClose();
1334
+ }
1335
+
1336
+ return { dispose, signal: abortController.signal };
1337
+ }
1338
+
1339
+ function readLambdaBodyWithAbort(
1340
+ reader: ReadableStreamDefaultReader<Uint8Array>,
1341
+ signal: AbortSignal,
1342
+ ): Promise<ReadableStreamReadResult<Uint8Array>> {
1343
+ if (signal.aborted) {
1344
+ return Promise.reject(lambdaAbortReason(signal));
1345
+ }
1346
+
1347
+ return new Promise((resolve, reject) => {
1348
+ const onAbort = (): void => {
1349
+ signal.removeEventListener("abort", onAbort);
1350
+ reject(lambdaAbortReason(signal));
1351
+ };
1352
+ signal.addEventListener("abort", onAbort, { once: true });
1353
+ void reader.read().then(
1354
+ (result) => {
1355
+ signal.removeEventListener("abort", onAbort);
1356
+ resolve(result);
1357
+ },
1358
+ (error: unknown) => {
1359
+ signal.removeEventListener("abort", onAbort);
1360
+ reject(error);
1361
+ },
1362
+ );
1130
1363
  });
1131
1364
  }
1132
1365
 
1366
+ function removeLambdaStreamListener(
1367
+ stream: AwsLambdaStreamingResponseStream,
1368
+ event: "close" | "drain" | "error",
1369
+ listener: (error?: unknown) => void,
1370
+ ): void {
1371
+ if (typeof stream.off === "function") {
1372
+ stream.off(event, listener);
1373
+ return;
1374
+ }
1375
+ stream.removeListener?.(event, listener);
1376
+ }
1377
+
1378
+ function addLambdaStreamListener(
1379
+ stream: AwsLambdaStreamingResponseStream,
1380
+ event: "close" | "drain" | "error",
1381
+ listener: (error?: unknown) => void,
1382
+ ): void {
1383
+ const once = stream.once as
1384
+ | ((event: "close" | "drain" | "error", listener: (error?: unknown) => void) => unknown)
1385
+ | undefined;
1386
+ try {
1387
+ once?.call(stream, event, listener);
1388
+ } catch {
1389
+ // A structurally compatible drain-only stream may reject lifecycle event names.
1390
+ }
1391
+ }
1392
+
1393
+ function lambdaAbortReason(signal: AbortSignal): Error {
1394
+ return signal.reason instanceof Error
1395
+ ? signal.reason
1396
+ : new Error("AWS Lambda response streaming was aborted.");
1397
+ }
1398
+
1133
1399
  interface AwsLambdaRuntime {
1134
1400
  HttpResponseStream: {
1135
1401
  from(
@@ -1143,8 +1409,7 @@ interface AwsLambdaRuntime {
1143
1409
  }
1144
1410
 
1145
1411
  function awsLambdaRuntime(): AwsLambdaRuntime {
1146
- const runtime = (globalThis as { awslambda?: Partial<AwsLambdaRuntime> | undefined })
1147
- .awslambda;
1412
+ const runtime = (globalThis as { awslambda?: Partial<AwsLambdaRuntime> | undefined }).awslambda;
1148
1413
 
1149
1414
  if (
1150
1415
  typeof runtime?.streamifyResponse !== "function" ||
@@ -1202,13 +1467,14 @@ function splitSetCookieHeader(header: string): string[] {
1202
1467
  return cookies.filter((cookie) => cookie.length > 0);
1203
1468
  }
1204
1469
 
1205
- function isTextContentType(contentType: string | null): boolean {
1470
+ function isUtf8TextContentType(contentType: string | null): boolean {
1206
1471
  if (contentType === null) {
1207
- return true;
1472
+ return false;
1208
1473
  }
1209
1474
 
1210
- const type = contentType.toLowerCase().split(";")[0]?.trim() ?? "";
1211
- return (
1475
+ const [rawType = "", ...parameters] = contentType.split(";");
1476
+ const type = rawType.trim().toLowerCase();
1477
+ const textual =
1212
1478
  type.startsWith("text/") ||
1213
1479
  type === "application/json" ||
1214
1480
  type === "application/javascript" ||
@@ -1216,6 +1482,51 @@ function isTextContentType(contentType: string | null): boolean {
1216
1482
  type === "application/xml" ||
1217
1483
  type === "application/x-www-form-urlencoded" ||
1218
1484
  type.endsWith("+json") ||
1219
- type.endsWith("+xml")
1485
+ type.endsWith("+xml");
1486
+ if (!textual) {
1487
+ return false;
1488
+ }
1489
+
1490
+ let hasCharset = false;
1491
+ for (const parameter of parameters) {
1492
+ const equals = parameter.indexOf("=");
1493
+ const name = (equals === -1 ? parameter : parameter.slice(0, equals)).trim().toLowerCase();
1494
+ if (name !== "charset") {
1495
+ continue;
1496
+ }
1497
+
1498
+ hasCharset = true;
1499
+ if (equals === -1) {
1500
+ return false;
1501
+ }
1502
+
1503
+ const value = unquoteContentTypeParameter(parameter.slice(equals + 1).trim()).toLowerCase();
1504
+ if (value !== "utf-8" && value !== "utf8") {
1505
+ return false;
1506
+ }
1507
+ }
1508
+
1509
+ if (hasCharset) {
1510
+ return true;
1511
+ }
1512
+
1513
+ return (
1514
+ type === "application/json" ||
1515
+ type === "application/javascript" ||
1516
+ type === "application/x-javascript" ||
1517
+ type === "text/javascript" ||
1518
+ type.endsWith("+json")
1220
1519
  );
1221
1520
  }
1521
+
1522
+ function unquoteContentTypeParameter(value: string): string {
1523
+ if (value.length < 2) {
1524
+ return value;
1525
+ }
1526
+
1527
+ const first = value[0];
1528
+ const last = value.at(-1);
1529
+ return (first === '"' && last === '"') || (first === "'" && last === "'")
1530
+ ? value.slice(1, -1)
1531
+ : value;
1532
+ }