Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/graceful-actions-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Return 404 for non-existent actions instead of throwing an unhandled error
21 changes: 19 additions & 2 deletions packages/astro/src/actions/runtime/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { z } from 'zod';
import type { Pipeline } from '../../core/base-pipeline.js';
import { shouldAppendForwardSlash } from '../../core/build/util.js';
import { AstroError } from '../../core/errors/errors.js';
import { ActionCalledFromServerError } from '../../core/errors/errors-data.js';
import { ActionCalledFromServerError, ActionNotFoundError } from '../../core/errors/errors-data.js';
import { removeTrailingForwardSlash } from '../../core/path.js';
import { apiContextRoutesSymbol } from '../../core/render-context.js';
import type { APIContext } from '../../types/public/index.js';
Expand Down Expand Up @@ -290,7 +290,24 @@ export function getActionContext(context: APIContext): AstroActionContext {
? removeTrailingForwardSlash(callerInfo.name)
: callerInfo.name;

const baseAction = await pipeline.getAction(callerInfoName);
let baseAction;
try {
baseAction = await pipeline.getAction(callerInfoName);
} catch (error) {
// Check if this is an ActionNotFoundError by comparing the name property
// We use this approach instead of instanceof because the error might be
// a different instance of the AstroError class depending on the environment
if (
error instanceof Error &&
'name' in error &&
typeof error.name === 'string' &&
error.name === ActionNotFoundError.name
) {
return { data: undefined, error: new ActionError({ code: 'NOT_FOUND' }) };
}
throw error;
}

let input;
try {
input = await parseRequestBody(context.request);
Expand Down
27 changes: 27 additions & 0 deletions packages/astro/test/actions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,19 @@ describe('Astro Actions', () => {
}
});

it('Returns 404 for non-existent action', async () => {
const res = await fixture.fetch('/_actions/nonExistent', {
method: 'POST',
body: JSON.stringify({}),
headers: {
'Content-Type': 'application/json',
},
});
assert.equal(res.status, 404);
const data = await res.json();
assert.equal(data.code, 'NOT_FOUND');
});

it('Should fail when calling an action without using Astro.callAction', async () => {
const res = await fixture.fetch('/invalid/');
const text = await res.text();
Expand Down Expand Up @@ -537,6 +550,20 @@ describe('Astro Actions', () => {
assert.equal(data, 'Hello, ben!');
}
});

it('Returns 404 for non-existent action', async () => {
const req = new Request('http://example.com/_actions/nonExistent', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({}),
});
const res = await app.render(req);
assert.equal(res.status, 404);
const data = await res.json();
assert.equal(data.code, 'NOT_FOUND');
});
});
});

Expand Down
Loading