diff --git a/.changeset/many-guests-yell.md b/.changeset/many-guests-yell.md new file mode 100644 index 0000000000..2a31af25ac --- /dev/null +++ b/.changeset/many-guests-yell.md @@ -0,0 +1,5 @@ +--- +"astro": patch +--- + +Fixes 500 errors when sending empty params or returning an empty response from an action. diff --git a/packages/astro/src/actions/runtime/middleware.ts b/packages/astro/src/actions/runtime/middleware.ts index ba582c21db..3241e0ac40 100644 --- a/packages/astro/src/actions/runtime/middleware.ts +++ b/packages/astro/src/actions/runtime/middleware.ts @@ -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'] = { diff --git a/packages/astro/src/actions/runtime/route.ts b/packages/astro/src/actions/runtime/route.ts index d6e89b7390..e1521f3ad9 100644 --- a/packages/astro/src/actions/runtime/route.ts +++ b/packages/astro/src/actions/runtime/route.ts @@ -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', }, diff --git a/packages/astro/src/actions/runtime/utils.ts b/packages/astro/src/actions/runtime/utils.ts index 8beb43a5a1..10f09665b1 100644 --- a/packages/astro/src/actions/runtime/utils.ts +++ b/packages/astro/src/actions/runtime/utils.ts @@ -12,16 +12,16 @@ export type MaybePromise = T | Promise; export async function getAction( pathKeys: string[] -): Promise<(param: unknown) => MaybePromise> { +): Promise<((param: unknown) => MaybePromise) | 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; } diff --git a/packages/astro/templates/actions.mjs b/packages/astro/templates/actions.mjs index cd7caa714e..a31c51c3af 100644 --- a/packages/astro/templates/actions.mjs +++ b/packages/astro/templates/actions.mjs @@ -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; } diff --git a/packages/astro/test/actions.test.js b/packages/astro/test/actions.test.js index 76f296dda3..3d632dd66f 100644 --- a/packages/astro/test/actions.test.js +++ b/packages/astro/test/actions.test.js @@ -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); + }); }); }); diff --git a/packages/astro/test/fixtures/actions/src/actions/index.ts b/packages/astro/test/fixtures/actions/src/actions/index.ts index 8c49e6d9b1..c196c1ae3e 100644 --- a/packages/astro/test/fixtures/actions/src/actions/index.ts +++ b/packages/astro/test/fixtures/actions/src/actions/index.ts @@ -50,4 +50,9 @@ export const server = { return locals.user; } }), + fireAndForget: defineAction({ + handler: async () => { + return; + } + }), };