From 837ee3a4aa6b33362bd680d4a7fc786ed8639444 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Tue, 17 Sep 2024 15:13:49 -0400 Subject: [PATCH] Encode action result in cookie (#12016) * Encode action result in cookie * Add a changeset --- .changeset/rotten-phones-scream.md | 5 +++ packages/astro/e2e/actions-blog.test.js | 19 ++++++++ .../actions-blog/src/actions/index.ts | 19 ++++++++ .../src/pages/lots-of-fields.astro | 43 +++++++++++++++++++ .../astro/src/actions/runtime/middleware.ts | 17 +++++--- .../src/actions/runtime/virtual/shared.ts | 21 +++++++-- 6 files changed, 115 insertions(+), 9 deletions(-) create mode 100644 .changeset/rotten-phones-scream.md create mode 100644 packages/astro/e2e/fixtures/actions-blog/src/pages/lots-of-fields.astro diff --git a/.changeset/rotten-phones-scream.md b/.changeset/rotten-phones-scream.md new file mode 100644 index 000000000000..8514fc7062c3 --- /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 d9c1bc1dfd68..e1f5907e0f72 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 43ffb43d42d4..c58ccdf66bae 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 000000000000..2b78aee1b869 --- /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); +--- + + + + Actions + + + +
+ + {result?.error?.fields.one} + + {result?.error?.fields.two} + + {result?.error?.fields.three} + + {result?.error?.fields.four} + + {result?.error?.fields.five} + + {result?.error?.fields.six} + + {result?.error?.fields.seven} + + {result?.error?.fields.eight} + + {result?.error?.fields.nine} + + {result?.error?.fields.ten} + +
+ + diff --git a/packages/astro/src/actions/runtime/middleware.ts b/packages/astro/src/actions/runtime/middleware.ts index b51322b1de4e..dae0a3811934 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; }) { - 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 8367710b94ed..2171dabe71bb 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): SerializedActi if (import.meta.env?.DEV) { actionResultErrorStack.set(res.error.stack); } + + let body: Record; + 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,