withastro/astro · error · Error

[astro:actions] `defineAction()` unexpectedly used on the cl

Error message

[astro:actions] `defineAction()` unexpectedly used on the client.

What it means

The client-side entrypoint of the astro:actions virtual module is a stub that throws if defineAction() is ever executed in a browser bundle. defineAction() is server-only; reaching this stub means server action-definition code leaked into the client module graph (an import that should not exist).

Source

Thrown at packages/astro/src/actions/runtime/entrypoints/client.ts:28

} from '../client.js';

export { ACTION_QUERY_PARAMS } from '../../consts.js';
export {
	ActionError,
	isActionError,
	isInputError,
} from '../client.js';
export type {
	ActionAPIContext,
	ActionClient,
	ActionErrorCode,
	ActionInputSchema,
	ActionReturnType,
	SafeResult,
} from '../types.js';

export function defineAction() {
	throw new Error('[astro:actions] `defineAction()` unexpectedly used on the client.');
}

export function getActionContext() {
	throw new Error('[astro:actions] `getActionContext()` unexpectedly used on the client.');
}

export const getActionPath = createGetActionPath({
	baseUrl: import.meta.env.BASE_URL,
	shouldAppendTrailingSlash,
});

export const actions = createActionsProxy({
	handleAction: async (param, path) => {
		const headers = new Headers();
		headers.set('Accept', 'application/json');
		// Apply adapter-specific headers for internal fetches
		for (const [key, value] of Object.entries(internalFetchHeaders)) {
			headers.set(key, value);

View on GitHub (pinned to d081033d5f)

Solutions

  1. Move zod input schemas into a separate schemas.ts with no defineAction(), and import only that from client code.
  2. In client components, import only the type: import type { actions } from '...'; (type-only imports are erased).
  3. Audit the client module graph (vite build / astro inspect) to confirm no action-definition file reaches the browser.

Example fix

// before - src/actions/index.ts
import { defineAction } from 'astro:actions';
import { z } from 'astro:schema';
const schema = z.object({ email: z.string().email() });
export const subscribe = defineAction({ input: schema, handler: async (i) => i });
// client component imports { schema } from '@/actions' -> pulls defineAction

// after - split schema into src/actions/schemas.ts
import { z } from 'astro:schema';
export const subscribeSchema = z.object({ email: z.string().email() });
// client imports { subscribeSchema } from '@/actions/schemas' (no defineAction)
Defensive patterns

Strategy: validation

Validate before calling

// Build-time guard: fail if a client entrypoint imports a module that calls defineAction.
// Example using a simple check in a vite plugin or a lint rule over the client entry graph:
const CLIENT_FORBIDDEN = /defineAction\s*\(/;
// scan files reachable from src/ that are imported by client.entry.ts
for (const file of clientReachableFiles) {
  if (CLIENT_FORBIDDEN.test(readFile(file))) {
    throw new Error(`${file} calls defineAction() but is reachable from a client entry`);
  }
}

Prevention

When it happens

Trigger: Importing your actions/index.ts (or any module that calls defineAction()) from a client script, a framework island (React/Vue/Svelte client component), or a client entrypoint. The bundler then resolved astro:actions to the client stub.

Common situations: Co-locating a zod schema and defineAction() in one file, then importing that file into a client component to reuse the schema; barrel exports that re-export server action modules; importing actions for "just the types" but pulling runtime code along.

Related errors


AI-assisted analysis of withastro/astro@d081033d5f (2026-08-12). Data as JSON: /api/errors/50e7511f6be974f6. Report an issue: GitHub.