0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2025-01-27 22:19:04 -05:00

Actions: support empty args and empty response (#11041)

* feat: support empty args and empty response

* chore: changeset

* Update .changeset/many-guests-yell.md

Co-authored-by: Florian Lefebvre <contact@florian-lefebvre.dev>

---------

Co-authored-by: Florian Lefebvre <contact@florian-lefebvre.dev>
This commit is contained in:
Ben Holmes 2024-05-15 07:42:04 -04:00 committed by GitHub
parent d0d1710439
commit 6cc3fb97ec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 39 additions and 4 deletions

View file

@ -0,0 +1,5 @@
---
"astro": patch
---
Fixes 500 errors when sending empty params or returning an empty response from an action.

View file

@ -27,6 +27,8 @@ export const onRequest = defineMiddleware(async (context, next) => {
const actionPathKeys = actionPath.replace('/_actions/', '').split('.');
const action = await getAction(actionPathKeys);
if (!action) return nextWithLocalsStub(next, locals);
const result = await ApiContextStorage.run(context, () => callSafely(() => action(formData)));
const actionsInternal: Locals['_actionsInternal'] = {

View file

@ -7,9 +7,15 @@ export const POST: APIRoute = async (context) => {
const { request, url } = context;
const actionPathKeys = url.pathname.replace('/_actions/', '').split('.');
const action = await getAction(actionPathKeys);
if (!action) {
return new Response(null, { status: 404 });
}
const contentType = request.headers.get('Content-Type');
const contentLength = request.headers.get('Content-Length');
let args: unknown;
if (contentType && hasContentType(contentType, formContentTypes)) {
if (contentLength === '0') {
args = undefined;
} else if (contentType && hasContentType(contentType, formContentTypes)) {
args = await request.clone().formData();
} else if (contentType && hasContentType(contentType, ['application/json'])) {
args = await request.clone().json();
@ -35,6 +41,7 @@ export const POST: APIRoute = async (context) => {
);
}
return new Response(JSON.stringify(result.data), {
status: result.data ? 200 : 204,
headers: {
'Content-Type': 'application/json',
},

View file

@ -12,16 +12,16 @@ export type MaybePromise<T> = T | Promise<T>;
export async function getAction(
pathKeys: string[]
): Promise<(param: unknown) => MaybePromise<unknown>> {
): Promise<((param: unknown) => MaybePromise<unknown>) | undefined> {
let { server: actionLookup } = await import(import.meta.env.ACTIONS_PATH);
for (const key of pathKeys) {
if (!(key in actionLookup)) {
throw new Error('Action not found');
return undefined;
}
actionLookup = actionLookup[key];
}
if (typeof actionLookup !== 'function') {
throw new Error('Action not found');
return undefined;
}
return actionLookup;
}

View file

@ -45,6 +45,7 @@ async function actionHandler(clientParam, path) {
});
}
headers.set('Content-Type', 'application/json');
headers.set('Content-Length', body?.length.toString() ?? '0');
}
const res = await fetch(path, {
method: 'POST',
@ -54,6 +55,9 @@ async function actionHandler(clientParam, path) {
if (!res.ok) {
throw await ActionError.fromResponse(res);
}
// Check if response body is empty before parsing.
if (res.status === 204) return;
const json = await res.json();
return json;
}

View file

@ -202,5 +202,17 @@ describe('Astro Actions', () => {
assert.equal($('#error-message').text(), 'Not logged in');
assert.equal($('#error-code').text(), 'UNAUTHORIZED');
});
it('Sets status to 204 when no content', async () => {
const req = new Request('http://example.com/_actions/fireAndForget', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': '0',
},
});
const res = await app.render(req);
assert.equal(res.status, 204);
});
});
});

View file

@ -50,4 +50,9 @@ export const server = {
return locals.user;
}
}),
fireAndForget: defineAction({
handler: async () => {
return;
}
}),
};