withastro/astro · error · AstroError

MissingMiddlewareForInternationalization

MissingMiddlewareForInternationalization

Error message

Your configuration setting `i18n.routing: 'manual'` requires you to provide your own i18n `middleware` file.

What it means

When no user middleware file exists, Astro's Vite middleware plugin serves a generated noop entry (NOOP_MIDDLEWARE). With i18n.routing set to 'manual', Astro refuses that noop: manual routing means Astro performs no locale routing for you, so a middleware file of your own is mandatory, and AstroErrorData.MissingMiddlewareForInternationalization is thrown from the plugin's load hook.

Solutions

  1. Create src/middleware.ts that applies Astro's i18n middleware: export const onRequest = sequence(middleware({ redirectToDefaultLocale: true, prefixDefaultLocale: false })) using imports from 'astro:i18n' and 'astro:middleware'
  2. Or write your own routing middleware exporting onRequest and implement the locale handling manually
  3. Verify the file location and export: it must live at the srcDir root and export onRequest (or onPreparation)
  4. If you did not intend manual routing, adjust the i18n config instead of keeping 'manual' without middleware

Example fix

// before: astro.config.mjs
export default defineConfig({ i18n: { defaultLocale: 'en', locales: ['en', 'fr'], routing: 'manual' } });
// and no src/middleware.ts

// after: create src/middleware.ts
import { sequence } from 'astro:middleware';
import { middleware } from 'astro:i18n';

export const onRequest = sequence(
  middleware({
    redirectToDefaultLocale: true,
    prefixDefaultLocale: false,
  })
);
Defensive patterns

Strategy: validation

Validate before calling

// run as a preflight/CI check in Node
import { existsSync } from 'node:fs';
import config from './astro.config.mjs';

if (config.i18n?.routing === 'manual' && !existsSync('./src/middleware.ts')) {
  throw new Error('i18n.routing "manual" requires src/middleware.ts exporting onRequest');
}

Prevention

When it happens

Trigger: Configuring i18n with routing: 'manual' (in Astro 5 this is the default once i18n is configured) while the project has no src/middleware.ts/js exporting onRequest; the middleware file was renamed, moved out of srcDir, or lost in a merge.

Common situations: Upgrading to Astro 5, where manual routing became the default and previously optional middleware became required; enabling i18n on an existing project that never had middleware; CI checkout where src/middleware.ts is not committed.

Related errors


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

Appendix: source

Thrown at packages/astro/src/core/middleware/vite-plugin.ts:80

				userMiddlewareIsPresent = !!middlewareId;
				if (middlewareId) {
					resolvedMiddlewareId = middlewareId.id;
					return MIDDLEWARE_RESOLVED_MODULE_ID;
				} else if (hasIntegrationMiddleware) {
					return MIDDLEWARE_RESOLVED_MODULE_ID;
				} else {
					return NOOP_MIDDLEWARE;
				}
			},
		},
		load: {
			filter: {
				id: new RegExp(`^(${NOOP_MIDDLEWARE}|${MIDDLEWARE_RESOLVED_MODULE_ID})$`),
			},
			async handler(id) {
				if (id === NOOP_MIDDLEWARE) {
					if (!userMiddlewareIsPresent && settings.config.i18n?.routing === 'manual') {
						throw new AstroError(MissingMiddlewareForInternationalization);
					}
					return { code: 'export const onRequest = (_, next) => next()' };
				}
				if (id === MIDDLEWARE_RESOLVED_MODULE_ID) {
					if (!userMiddlewareIsPresent && settings.config.i18n?.routing === 'manual') {
						throw new AstroError(MissingMiddlewareForInternationalization);
					}

					const preMiddleware = createMiddlewareImports(settings.middlewares.pre, 'pre');
					const postMiddleware = createMiddlewareImports(settings.middlewares.post, 'post');

					const code = `
				${
					userMiddlewareIsPresent
						? `import { onRequest as userOnRequest } from '${resolvedMiddlewareId}';`
						: ''
				}
import { sequence } from 'astro:middleware';

View on GitHub (pinned to 52e6c34790)