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:
parent
6860beb995
commit
837ee3a4aa
6 changed files with 115 additions and 9 deletions
5
.changeset/rotten-phones-scream.md
Normal file
5
.changeset/rotten-phones-scream.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'astro': patch
|
||||
---
|
||||
|
||||
Fixes actions with large amount of validation errors
|
|
@ -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/'));
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
})
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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>
|
|
@ -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');
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue