withastro/astro · error · AstroError

ActionNotFoundError

ActionNotFoundError

Error message

The server received a request for an action named `${actionName}` but could not find a match. If you renamed an action, check that you've updated your `actions/index` file and your calling code to match.

What it means

getAction() walks the dot-separated action path through the `server` actions object. Once an intermediate segment resolves to a function, that function is a leaf action: its own properties are not part of the action namespace. Requesting anything deeper than a leaf (e.g. `logout.fetch`) makes traversal step into a function's internals, which Astro rejects with ActionNotFoundError instead of leaking function properties.

Solutions

  1. Compare the requested dotted path against your actions/index.ts exports and remove the extra trailing segments
  2. If you intended nesting, wrap the leaf in an object namespace: move the action into a sub-object (folder or nested export) so the deeper key exists
  3. Bust stale client bundles: hard-reload / rebuild after renaming actions

Example fix

// before — action is a leaf, caller adds a sub-key
export const logout = defineAction({ handler: () => 'ok' });
await actions['logout.session'](); // throws

// after
await actions.logout();
Defensive patterns

Strategy: validation

Validate before calling

// centralize action names and validate against them before calling
const ACTION_PATHS = ['blog.like', 'blog.share', 'logout'] as const;
function isKnownActionPath(p: string): boolean {
  return (ACTION_PATHS as readonly string[]).includes(p);
}
if (!isKnownActionPath(name)) return new Response('Unknown action', { status: 404 });
await actions[name as keyof typeof actions](input);

Type guard

type ActionPath = 'blog.like' | 'blog.share' | 'logout';
function isActionPath(p: string): p is ActionPath {
  return ['blog.like', 'blog.share', 'logout'].includes(p);
}

Try / catch

try {
  await actions[name](input);
} catch (e) {
  if (e instanceof AstroError && e.code === 'ActionNotFoundError') {
    return new Response('Unknown action', { status: 404 });
  }
  throw e;
}

Prevention

When it happens

Trigger: A client or caller requests an action name with extra segments after a leaf action, e.g. POSTing actionName 'logout.clear' when 'logout' is already a single defineAction. Also stitching paths dynamically where a leaf name is accidentally prefixed to a sub-key, or double-joining with dots ('blog.like' + '.' + 'like').

Common situations: Renaming a nested folder action to a flat one while stale client code still calls the old nested path; typos adding a suffix after an action name; hand-built action-name strings in middleware or custom fetch wrappers.

Related errors


AI-assisted analysis of withastro/astro@52e6c34790 (2026-08-18). Data as JSON: /api/errors/904270df93fad83f. Report an issue: GitHub.

Appendix: source

Thrown at packages/astro/src/actions/load.ts:45

/** Looks up a single action handler by its dot-separated path. */
export async function getAction(
	manifest: SSRManifest,
	path: string,
): Promise<ActionClient<unknown, ActionAccept, $ZodType>> {
	const pathKeys = path.split('.').map((key) => decodeURIComponent(key));
	let { server } = await getActions(manifest);

	if (!server || !(typeof server === 'object')) {
		throw new TypeError(
			`Expected \`server\` export in actions file to be an object. Received ${typeof server}.`,
		);
	}

	for (const key of pathKeys) {
		// An action is a leaf: once resolved to a function, its own properties
		// are not part of the action namespace and cannot be traversed further.
		if (typeof server === 'function') {
			throw new AstroError({
				...ActionNotFoundError,
				message: ActionNotFoundError.message(pathKeys.join('.')),
			});
		}
		if (FORBIDDEN_PATH_KEYS.has(key)) {
			throw new AstroError({
				...ActionNotFoundError,
				message: ActionNotFoundError.message(pathKeys.join('.')),
			});
		}
		if (!Object.hasOwn(server, key)) {
			throw new AstroError({
				...ActionNotFoundError,
				message: ActionNotFoundError.message(pathKeys.join('.')),
			});
		}
		// @ts-expect-error we are doing a recursion... it's ugly
		server = server[key];

View on GitHub (pinned to 52e6c34790)