0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2024-12-16 21:46:22 -05:00

Encode action result in cookie (#12016)

* Encode action result in cookie

* Add a changeset
This commit is contained in:
Matthew Phillips 2024-09-17 15:13:49 -04:00 committed by GitHub
parent 6860beb995
commit 837ee3a4aa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 115 additions and 9 deletions

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Fixes actions with large amount of validation errors

View file

@ -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/'));

View file

@ -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;
}
})
},
};

View file

@ -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>

View file

@ -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');

View file

@ -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,