Skip to content

Commit 6cc3fb9

Browse files
bholmesdevflorian-lefebvre
andauthoredMay 15, 2024··
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>
1 parent d0d1710 commit 6cc3fb9

File tree

7 files changed

+39
-4
lines changed

7 files changed

+39
-4
lines changed
 

‎.changeset/many-guests-yell.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"astro": patch
3+
---
4+
5+
Fixes 500 errors when sending empty params or returning an empty response from an action.

‎packages/astro/src/actions/runtime/middleware.ts

+2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ export const onRequest = defineMiddleware(async (context, next) => {
2727

2828
const actionPathKeys = actionPath.replace('/_actions/', '').split('.');
2929
const action = await getAction(actionPathKeys);
30+
if (!action) return nextWithLocalsStub(next, locals);
31+
3032
const result = await ApiContextStorage.run(context, () => callSafely(() => action(formData)));
3133

3234
const actionsInternal: Locals['_actionsInternal'] = {

‎packages/astro/src/actions/runtime/route.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,15 @@ export const POST: APIRoute = async (context) => {
77
const { request, url } = context;
88
const actionPathKeys = url.pathname.replace('/_actions/', '').split('.');
99
const action = await getAction(actionPathKeys);
10+
if (!action) {
11+
return new Response(null, { status: 404 });
12+
}
1013
const contentType = request.headers.get('Content-Type');
14+
const contentLength = request.headers.get('Content-Length');
1115
let args: unknown;
12-
if (contentType && hasContentType(contentType, formContentTypes)) {
16+
if (contentLength === '0') {
17+
args = undefined;
18+
} else if (contentType && hasContentType(contentType, formContentTypes)) {
1319
args = await request.clone().formData();
1420
} else if (contentType && hasContentType(contentType, ['application/json'])) {
1521
args = await request.clone().json();
@@ -35,6 +41,7 @@ export const POST: APIRoute = async (context) => {
3541
);
3642
}
3743
return new Response(JSON.stringify(result.data), {
44+
status: result.data ? 200 : 204,
3845
headers: {
3946
'Content-Type': 'application/json',
4047
},

‎packages/astro/src/actions/runtime/utils.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,16 @@ export type MaybePromise<T> = T | Promise<T>;
1212

1313
export async function getAction(
1414
pathKeys: string[]
15-
): Promise<(param: unknown) => MaybePromise<unknown>> {
15+
): Promise<((param: unknown) => MaybePromise<unknown>) | undefined> {
1616
let { server: actionLookup } = await import(import.meta.env.ACTIONS_PATH);
1717
for (const key of pathKeys) {
1818
if (!(key in actionLookup)) {
19-
throw new Error('Action not found');
19+
return undefined;
2020
}
2121
actionLookup = actionLookup[key];
2222
}
2323
if (typeof actionLookup !== 'function') {
24-
throw new Error('Action not found');
24+
return undefined;
2525
}
2626
return actionLookup;
2727
}

‎packages/astro/templates/actions.mjs

+4
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ async function actionHandler(clientParam, path) {
4545
});
4646
}
4747
headers.set('Content-Type', 'application/json');
48+
headers.set('Content-Length', body?.length.toString() ?? '0');
4849
}
4950
const res = await fetch(path, {
5051
method: 'POST',
@@ -54,6 +55,9 @@ async function actionHandler(clientParam, path) {
5455
if (!res.ok) {
5556
throw await ActionError.fromResponse(res);
5657
}
58+
// Check if response body is empty before parsing.
59+
if (res.status === 204) return;
60+
5761
const json = await res.json();
5862
return json;
5963
}

‎packages/astro/test/actions.test.js

+12
Original file line numberDiff line numberDiff line change
@@ -202,5 +202,17 @@ describe('Astro Actions', () => {
202202
assert.equal($('#error-message').text(), 'Not logged in');
203203
assert.equal($('#error-code').text(), 'UNAUTHORIZED');
204204
});
205+
206+
it('Sets status to 204 when no content', async () => {
207+
const req = new Request('http://example.com/_actions/fireAndForget', {
208+
method: 'POST',
209+
headers: {
210+
'Content-Type': 'application/json',
211+
'Content-Length': '0',
212+
},
213+
});
214+
const res = await app.render(req);
215+
assert.equal(res.status, 204);
216+
});
205217
});
206218
});

‎packages/astro/test/fixtures/actions/src/actions/index.ts

+5
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,9 @@ export const server = {
5050
return locals.user;
5151
}
5252
}),
53+
fireAndForget: defineAction({
54+
handler: async () => {
55+
return;
56+
}
57+
}),
5358
};

0 commit comments

Comments
 (0)
Please sign in to comment.