@uniflowed/router 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -233,13 +233,13 @@ export function createActionDispatcher(options: {|
233
233
  }
234
234
 
235
235
  const body = await readBoundedText(request, MAX_ACTION_BODY_BYTES);
236
- if (body == null) {
237
- return refusal(413);
236
+ if (body.kind !== "text") {
237
+ return refusal(body.kind === "too-large" ? 413 : 400);
238
238
  }
239
239
 
240
240
  let args: Array<ActionArgument>;
241
241
  try {
242
- args = decodeActionArguments(body);
242
+ args = decodeActionArguments(body.value);
243
243
  } catch (error) {
244
244
  // The reason is the sender's own payload described back to them, which
245
245
  // is a thing to write in a log and not a thing to answer with.
@@ -344,10 +344,10 @@ async function nativeFormPost(
344
344
  // A copy, so that a form which turns out not to be an action post reaches the
345
345
  // route handler with its body unread.
346
346
  const text = await readBoundedText(request.clone(), MAX_ACTION_BODY_BYTES);
347
- if (text == null) {
347
+ if (text.kind !== "text") {
348
348
  return null;
349
349
  }
350
- const post = readFormPost(new URLSearchParams(text));
350
+ const post = readFormPost(new URLSearchParams(text.value));
351
351
  if (post == null) {
352
352
  return null;
353
353
  }
@@ -659,12 +659,20 @@ function isMedia(declared: string | null, expected: string): boolean {
659
659
  * each bad sequence with U+FFFD, which is `docs/security.md`'s row about
660
660
  * non-UTF-8 input applied at the one place uf decodes bytes a client sent.
661
661
  */
662
- async function readBoundedText(request: Request, limit: number): Promise<string | null> {
662
+ type BoundedText =
663
+ | {| readonly kind: "text", readonly value: string |}
664
+ | {| readonly kind: "too-large" |}
665
+ | {| readonly kind: "invalid" |};
666
+
667
+ async function readBoundedText(request: Request, limit: number): Promise<BoundedText> {
663
668
  const declared = request.headers.get("content-length");
664
669
  if (declared != null) {
665
670
  const size = Number(declared);
666
- if (!Number.isInteger(size) || size < 0 || size > limit) {
667
- return null;
671
+ if (!Number.isInteger(size) || size < 0) {
672
+ return { kind: "invalid" };
673
+ }
674
+ if (size > limit) {
675
+ return { kind: "too-large" };
668
676
  }
669
677
  }
670
678
 
@@ -675,7 +683,7 @@ async function readBoundedText(request: Request, limit: number): Promise<string
675
683
  // rather than written here.
676
684
  const body: ReadableStream | null = (request as $FlowFixMe).body;
677
685
  if (body == null) {
678
- return "";
686
+ return { kind: "text", value: "" };
679
687
  }
680
688
 
681
689
  const reader = body.getReader();
@@ -695,12 +703,12 @@ async function readBoundedText(request: Request, limit: number): Promise<string
695
703
  // declarations uf checks against require one, and "too large" is what
696
704
  // a cancelled read of an oversized body is about.
697
705
  await reader.cancel("the body is larger than a server action accepts");
698
- return null;
706
+ return { kind: "too-large" };
699
707
  }
700
708
  chunks.push(chunk);
701
709
  }
702
710
  } catch {
703
- return null;
711
+ return { kind: "invalid" };
704
712
  }
705
713
 
706
714
  const joined = new Uint8Array(total);
@@ -710,9 +718,9 @@ async function readBoundedText(request: Request, limit: number): Promise<string
710
718
  at += chunk.byteLength;
711
719
  }
712
720
  try {
713
- return new TextDecoder("utf-8", { fatal: true }).decode(joined);
721
+ return { kind: "text", value: new TextDecoder("utf-8", { fatal: true }).decode(joined) };
714
722
  } catch {
715
- return null;
723
+ return { kind: "invalid" };
716
724
  }
717
725
  }
718
726
 
@@ -24,12 +24,19 @@ export function hydrationOptions(
24
24
  recovery: ?Recovery,
25
25
  formState: FormState | void,
26
26
  ): HydrationOptions | void {
27
- if (recovery == null && formState == null) {
27
+ // React's default reporter dispatches a global error for a boundary it
28
+ // recovers by rendering on the client. Deno's Node compatibility layer
29
+ // treats that dispatch as an uncaught exception and exits the process.
30
+ // Keep the error visible without turning successful recovery into a crash.
31
+ const deno = "Deno" in globalThis;
32
+ if (recovery == null && formState == null && !deno) {
28
33
  return undefined;
29
34
  }
30
35
  const options: HydrationOptions = {};
31
36
  if (recovery != null) {
32
37
  options.onRecoverableError = recovery;
38
+ } else if (deno) {
39
+ options.onRecoverableError = (error) => console.error(error);
33
40
  }
34
41
  if (formState != null) {
35
42
  options.formState = formState;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniflowed/router",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "The file-system router for Flow React applications: matching, layouts, loaders, navigation, server rendering and hydration.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -73,8 +73,8 @@
73
73
  }
74
74
  },
75
75
  "dependencies": {
76
- "@uniflowed/hooks": "0.6.0",
77
- "@uniflowed/server": "0.6.0",
78
- "@uniflowed/validator": "0.6.0"
76
+ "@uniflowed/hooks": "0.8.0",
77
+ "@uniflowed/server": "0.8.0",
78
+ "@uniflowed/validator": "0.8.0"
79
79
  }
80
80
  }