summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.changeset/rotten-phones-scream.md5
-rw-r--r--packages/astro/e2e/actions-blog.test.js19
-rw-r--r--packages/astro/e2e/fixtures/actions-blog/src/actions/index.ts19
-rw-r--r--packages/astro/e2e/fixtures/actions-blog/src/pages/lots-of-fields.astro43
-rw-r--r--packages/astro/src/actions/runtime/middleware.ts17
-rw-r--r--packages/astro/src/actions/runtime/virtual/shared.ts21
6 files changed, 115 insertions, 9 deletions
diff --git a/.changeset/rotten-phones-scream.md b/.changeset/rotten-phones-scream.md
new file mode 100644
index 000000000..8514fc706
--- /dev/null
+++ b/.changeset/rotten-phones-scream.md
@@ -0,0 +1,5 @@
+---
+'astro': patch
+---
+
+Fixes actions with large amount of validation errors
diff --git a/packages/astro/e2e/actions-blog.test.js b/packages/astro/e2e/actions-blog.test.js
index d9c1bc1df..e1f5907e0 100644
--- a/packages/astro/e2e/actions-blog.test.js
+++ b/packages/astro/e2e/actions-blog.test.js
@@ -72,6 +72,25 @@ test.describe('Astro Actions - Blog', () => {
await expect(form.locator('p[data-error="body"]')).toBeVisible();
});
+ test('Comment action - progressive fallback lots of validation errors', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/lots-of-fields/'));
+
+ const form = page.getByTestId('lots');
+ const submitButton = form.getByRole('button');
+ await submitButton.click();
+
+ const expectedText = 'Expected string, received null';
+
+ const fields = [
+ 'one', 'two', 'three', 'four', 'five',
+ 'six', 'seven', 'eight', 'nine', 'ten'
+ ];
+
+ for await(const field of fields) {
+ await expect(form.locator(`.${field}.error`)).toHaveText(expectedText);
+ }
+ });
+
test('Comment action - progressive fallback success', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/blog/first-post/'));
diff --git a/packages/astro/e2e/fixtures/actions-blog/src/actions/index.ts b/packages/astro/e2e/fixtures/actions-blog/src/actions/index.ts
index 43ffb43d4..c58ccdf66 100644
--- a/packages/astro/e2e/fixtures/actions-blog/src/actions/index.ts
+++ b/packages/astro/e2e/fixtures/actions-blog/src/actions/index.ts
@@ -55,5 +55,24 @@ export const server = {
return comment;
},
}),
+
+ lotsOfStuff: defineAction({
+ accept: 'form',
+ input: z.object({
+ one: z.string().min(3),
+ two: z.string().min(3),
+ three: z.string().min(3),
+ four: z.string().min(3),
+ five: z.string().min(3),
+ six: z.string().min(3),
+ seven: z.string().min(3),
+ eight: z.string().min(3),
+ nine: z.string().min(3),
+ ten: z.string().min(3)
+ }),
+ handler(form) {
+ return form;
+ }
+ })
},
};
diff --git a/packages/astro/e2e/fixtures/actions-blog/src/pages/lots-of-fields.astro b/packages/astro/e2e/fixtures/actions-blog/src/pages/lots-of-fields.astro
new file mode 100644
index 000000000..2b78aee1b
--- /dev/null
+++ b/packages/astro/e2e/fixtures/actions-blog/src/pages/lots-of-fields.astro
@@ -0,0 +1,43 @@
+---
+export const prerender = false;
+import { actions } from 'astro:actions';
+
+const result = Astro.getActionResult(actions.blog.lotsOfStuff);
+---
+
+<html>
+ <head>
+ <title>Actions</title>
+ <style>
+ form {
+ display: grid;
+ grid-row-gap: 10px;
+ }
+ </style>
+ </head>
+ <body>
+ <form method="POST" action={actions.blog.lotsOfStuff} data-testid="lots">
+ <input type="text" name="one" value="">
+ <span class="one error">{result?.error?.fields.one}</span>
+ <input type="text" name="two" value="">
+ <span class="two error">{result?.error?.fields.two}</span>
+ <input type="text" name="three" value="">
+ <span class="three error">{result?.error?.fields.three}</span>
+ <input type="text" name="four" value="">
+ <span class="four error">{result?.error?.fields.four}</span>
+ <input type="text" name="five" value="">
+ <span class="five error">{result?.error?.fields.five}</span>
+ <input type="text" name="six" value="">
+ <span class="six error">{result?.error?.fields.six}</span>
+ <input type="text" name="seven" value="">
+ <span class="seven error">{result?.error?.fields.seven}</span>
+ <input type="text" name="eight" value="">
+ <span class="eight error">{result?.error?.fields.eight}</span>
+ <input type="text" name="nine" value="">
+ <span class="nine error">{result?.error?.fields.nine}</span>
+ <input type="text" name="ten" value="">
+ <span class="ten error">{result?.error?.fields.ten}</span>
+ <button type="submit">Submit</button>
+ </form>
+ </body>
+</html>
diff --git a/packages/astro/src/actions/runtime/middleware.ts b/packages/astro/src/actions/runtime/middleware.ts
index b51322b1d..dae0a3811 100644
--- a/packages/astro/src/actions/runtime/middleware.ts
+++ b/packages/astro/src/actions/runtime/middleware.ts
@@ -10,6 +10,7 @@ import {
type SerializedActionResult,
serializeActionResult,
} from './virtual/shared.js';
+import { encodeBase64, decodeBase64 } from '@oslojs/encoding';
export type ActionPayload = {
actionResult: SerializedActionResult;
@@ -20,6 +21,9 @@ export type Locals = {
_actionPayload: ActionPayload;
};
+const decoder = new TextDecoder();
+const encoder = new TextEncoder();
+
export const onRequest = defineMiddleware(async (context, next) => {
if (context.isPrerendered) {
if (context.request.method === 'POST') {
@@ -39,8 +43,10 @@ export const onRequest = defineMiddleware(async (context, next) => {
// so short circuit if already defined.
if (locals._actionPayload) return next();
- const actionPayload = context.cookies.get(ACTION_QUERY_PARAMS.actionPayload)?.json();
- if (actionPayload) {
+ const actionPayloadCookie = context.cookies.get(ACTION_QUERY_PARAMS.actionPayload)?.value;
+ if (actionPayloadCookie) {
+ const actionPayload = JSON.parse(decoder.decode(decodeBase64(actionPayloadCookie)));
+
if (!isActionPayload(actionPayload)) {
throw new Error('Internal: Invalid action payload in cookie.');
}
@@ -124,10 +130,11 @@ async function redirectWithResult({
actionName: string;
actionResult: SafeResult<any, any>;
}) {
- context.cookies.set(ACTION_QUERY_PARAMS.actionPayload, {
- actionName,
+ const cookieValue = encodeBase64(encoder.encode(JSON.stringify({
+ actionName: actionName,
actionResult: serializeActionResult(actionResult),
- });
+ })));
+ context.cookies.set(ACTION_QUERY_PARAMS.actionPayload, cookieValue);
if (actionResult.error) {
const referer = context.request.headers.get('Referer');
diff --git a/packages/astro/src/actions/runtime/virtual/shared.ts b/packages/astro/src/actions/runtime/virtual/shared.ts
index 8367710b9..2171dabe7 100644
--- a/packages/astro/src/actions/runtime/virtual/shared.ts
+++ b/packages/astro/src/actions/runtime/virtual/shared.ts
@@ -204,14 +204,26 @@ export function serializeActionResult(res: SafeResult<any, any>): SerializedActi
if (import.meta.env?.DEV) {
actionResultErrorStack.set(res.error.stack);
}
+
+ let body: Record<string, any>;
+ if(res.error instanceof ActionInputError) {
+ body = {
+ type: res.error.type,
+ issues: res.error.issues,
+ fields: res.error.fields
+ };
+ } else {
+ body = {
+ ...res.error,
+ message: res.error.message
+ };
+ }
+
return {
type: 'error',
status: res.error.status,
contentType: 'application/json',
- body: JSON.stringify({
- ...res.error,
- message: res.error.message,
- }),
+ body: JSON.stringify(body),
};
}
if (res.data === undefined) {
@@ -252,6 +264,7 @@ export function deserializeActionResult(res: SerializedActionResult): SafeResult
let json;
try {
json = JSON.parse(res.body);
+
} catch {
return {
data: undefined,