Middleware
Purpose and Scope
Middleware is Astro’s request-time extension point: code that runs between an incoming request and the page, endpoint, redirect, or fallback route that will produce the final response. In user projects, the public convention is to create src/middleware.js, src/middleware.ts, or an index file under src/middleware/, then export a named onRequest function. That function receives an Astro API context and a next() function. It can mutate request-scoped data, return its own Response, or pass control to the rest of the rendering pipeline.
The repository implementation treats middleware as part of the core render pipeline rather than as a separate plugin system. AstroMiddleware owns the per-render execution of internal middleware plus the user middleware resolved from the pipeline, while callMiddleware enforces the contract that a middleware must either return a Response or delegate to next(). This design is important because middleware runs before different route kinds, including pages and endpoints, and because rewrites can re-enter routing. The same machinery also supports internal runtime features such as font file serving during development.
Sources: packages/astro/src/core/middleware/astro-middleware.ts, packages/astro/src/core/middleware/callMiddleware.ts, packages/astro/src/assets/fonts/core/font-file-middleware.ts
Relevant Source Files
packages/astro/src/core/middleware/index.ts- Defines the low-level middleware context creation API, including request, params, locale helpers, cookies, redirects, locals, action helpers, and locals serialization checks.packages/astro/src/assets/fonts/core/font-file-middleware.ts- Implements an internal middleware-like handler for font file requests, translating Node responses to a minimal interface and serving fetched font buffers with headers.packages/astro/src/core/middleware/astro-middleware.ts- Contains theAstroMiddlewareclass that composes internal and user middleware, dispatches to route rendering, applies rewrites, finalizes responses, and handles loop protection.packages/astro/src/core/middleware/callMiddleware.ts- Enforces the runtime middleware return contract and builds thenext()function passed into middleware handlers.packages/astro/src/core/middleware/defineMiddleware.ts- Provides the type-preservingdefineMiddleware()helper used by public middleware authoring APIs.packages/astro/src/core/middleware/noop-middleware.ts- Exports a pass-through middleware handler used when a middleware slot should simply callnext()and return the response.
Public Authoring Model
Astro’s public middleware model is intentionally small. A project exports onRequest, and that handler can inspect context.request, write values into context.locals, read cookies, redirect, or continue with next(). The official docs describe locals as request-specific information shared with endpoints and .astro routes during rendering, and the source reinforces that by making locals part of the generated APIContext. The context object is created around the incoming Request, route params, locale configuration, and optional platform-provided client address, so middleware authors work with the same request data used elsewhere in Astro.
The TypeScript helper defineMiddleware() does not wrap or alter the function at runtime; it returns the handler it receives. Its value is the public type surface: by importing the helper from the middleware module, authors get a checked handler shape without adding runtime indirection. The docs also expose sequence() for composing several middleware handlers in a declared order. In the core runtime, sequencing appears where Astro combines internal middleware with the project middleware before invoking the composed handler, so user composition and internal composition share the same conceptual model.
Sources: packages/astro/src/core/middleware/index.ts, packages/astro/src/core/middleware/defineMiddleware.ts, packages/astro/src/core/middleware/astro-middleware.ts
import { defineMiddleware } from 'astro:middleware';
export const onRequest = defineMiddleware(async (context, next) => {
context.locals.title = 'New title';
return next();
});API Context and Locals
The low-level createContext() function builds an APIContext for middleware from a CreateContext payload. That payload includes the incoming request, optional params, locale inputs, an initial locals object, and an optional clientAddress supplied by a trusted adapter or platform. The resulting context includes cookies, request metadata, generator information, route data placeholders, a redirect() helper that creates a response with a Location header, locale getters, origin pathname access, and disabled cache support for this constructed context.
locals is guarded carefully because it is the shared mutable object that middleware uses to communicate with downstream rendering. The getter verifies that the value is an object and throws if it is not. The setter throws on reassignment, which means middleware should mutate properties on context.locals rather than replace the object wholesale. This matches the public docs pattern of assigning context.locals.title or context.locals.property. The source also attaches action helpers by creating getActionResult from context.locals and callAction from the context, so middleware state participates in Astro’s broader server API surface.
Locale and address handling show how middleware remains portable across static, server, and adapter-driven environments. Preferred locale and locale list are computed lazily from the request and user-defined locales, while the current locale is derived from the route, supported locales, and default locale. clientAddress is available only when supplied from a trusted source; otherwise accessing it throws a static-client-address error. This keeps the API honest: middleware can use request-derived behavior where available, but platform-sensitive fields must be provided by the runtime integration.
Sources: packages/astro/src/core/middleware/index.ts
Runtime Execution Flow
At render time, AstroMiddleware.handle() marks middleware as a used pipeline feature, resolves props, creates the API context from the fetch state, and increments a per-state counter. The counter protects rewrite-heavy flows from cycling indefinitely: when the counter reaches the configured loop threshold, the handler returns a 508 response with an explanatory status text. This matters because middleware can trigger rewrites through next() payloads, and rewrites can cause the pipeline to reconsider routing with updated state.
The next callback constructed inside AstroMiddleware.handle() is the bridge from middleware to route dispatch. When a rewrite payload is provided, Astro logs the rewrite, asks the pipeline to resolve it, and applies the rewrite result back into the fetch state. After that, or when there is no rewrite payload, route rendering is delegated to the caller-provided renderRouteCallback. The middleware layer therefore does not decide whether the final target is a page, endpoint, redirect, or fallback. It coordinates pre-route behavior and then hands off to the route handler that owns dispatch.
If state.skipMiddleware is set, Astro bypasses middleware and calls the route callback directly. Otherwise it asks the pipeline for the user middleware, composes all internal middleware plus the user middleware, and invokes the resulting handler through callMiddleware(). After a response is produced, the middleware handler finalizes it and stores it on state.response. This sequencing explains why middleware can modify response headers after next() returns, but still depends on the route callback to produce the underlying page or endpoint response when the middleware does not short-circuit.
Sources: packages/astro/src/core/middleware/astro-middleware.ts, packages/astro/src/core/middleware/callMiddleware.ts
Handler Contract and Error Cases
callMiddleware() is the runtime gatekeeper for the middleware contract. It constructs a next() function that records whether delegation happened and stores the promise returned by the downstream response function. Then it calls the middleware handler and normalizes the result. The valid outcomes are intentionally narrow: a handler may return a Response without calling next(), or it may call next() and return either the downstream response or a new Response based on it. Anything outside that shape becomes an Astro error.
The important edge cases are explicit in the implementation. If the middleware calls next() and returns a non-Response value, Astro throws MiddlewareNotAResponse. If it calls next() but returns nothing, Astro returns the downstream response promise when one exists. If it neither calls next() nor returns a value, Astro throws MiddlewareNoDataOrNextCalled. These checks turn ambiguous middleware bugs into deterministic framework errors, which is especially helpful when a project mixes authentication, redirects, rewrites, and response post-processing in the same chain.
A correct pass-through middleware can therefore be very small: await next(), optionally inspect or modify the response, and return a Response. The repository’s NOOP_MIDDLEWARE_FN is the minimal built-in example. It accepts a context and next, awaits next(), and returns the result unchanged. That implementation is useful as a default because it exercises the same contract as user middleware while intentionally adding no behavior. It also shows that middleware composition can always include a valid handler even when no project-specific work is needed.
Sources: packages/astro/src/core/middleware/callMiddleware.ts, packages/astro/src/core/middleware/noop-middleware.ts
export const onRequest = async (context, next) => {
const response = await next();
response.headers.set('x-powered-by', 'astro');
return response;
};Internal Middleware Example: Font Files
The font file middleware shows how the same request-interception idea is used internally for an Astro feature. It receives a URL, a minimal response object, dependency hooks for fetching and identifying fonts, a logger, and a map from font IDs to font file metadata. If any font dependency is not initialized, if there is no URL, or if the request path does not match a known font ID, the handler calls next() and leaves the request to the rest of the stack. That is the same short-circuit-or-delegate pattern used by public middleware.
When the font middleware does handle a request, it sets development-friendly cache headers, fetches the font buffer, writes Content-Length and Content-Type, sets status 200, and ends the response with the buffer. On failures it logs a high-level error, formats Astro errors when available, then returns a 500 response through the minimal response interface. resToMinimalResponse() adapts Node’s ServerResponse to the smaller interface expected by the font middleware, which keeps this feature decoupled from the full Node response API while still fitting into Astro’s development server behavior.
Sources: packages/astro/src/assets/fonts/core/font-file-middleware.ts
Implementation Details and Next Steps
For application authors, the practical rule is to keep middleware focused on request-scoped decisions: authentication checks, redirects, locale selection, headers, cookies, and values that belong in context.locals. Do not reassign context.locals; mutate its properties. Always return a Response or return the result of next(). When composing multiple handlers, put validation and early redirects before handlers that depend on authenticated state, and place response post-processing after await next() so it can see the route result.
For integration and runtime contributors, the key code path starts in AstroMiddleware.handle(), moves through sequence(), and is validated by callMiddleware(). Context construction lives in index.ts, while helper exports such as defineMiddleware() preserve the public authoring surface. Internal features can follow the font middleware pattern when they need request interception but should delegate quickly for unrelated paths. Read the routing, endpoints, actions, sessions, and configuration pages next to understand the route callback, server APIs, and platform configuration that middleware commonly coordinates with.
Sources: packages/astro/src/core/middleware/index.ts, packages/astro/src/core/middleware/astro-middleware.ts, packages/astro/src/core/middleware/callMiddleware.ts, packages/astro/src/core/middleware/defineMiddleware.ts, packages/astro/src/assets/fonts/core/font-file-middleware.ts