diff options
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, |