Astro shows “The middleware didn’t return a Response” when your onRequest function in src/middleware.ts ends without returning anything. The message explains: “Make sure your middleware returns a Response object, either directly or by returning the Response from calling the next function.” The usual bug is calling next() without return, or a code path (often an if branch) that falls through.
Key Takeaways
- Every path through
onRequestmustreturn next()orreturnaResponse.await next()gives you the response to modify; you still have to return it.- Redirects:
return context.redirect('/login/').- With
sequence(), each middleware follows the same rule.
The common bug
// src/middleware.ts
import { defineMiddleware } from 'astro:middleware';
export const onRequest = defineMiddleware(async (context, next) => {
context.locals.startedAt = Date.now();
next(); // called, but not returned
});
The fix
export const onRequest = defineMiddleware(async (context, next) => {
context.locals.startedAt = Date.now();
return next();
});
Branches that fall through
// Wrong: when the user is logged in, nothing is returned
export const onRequest = defineMiddleware(async (context, next) => {
if (!context.cookies.has('session') && context.url.pathname.startsWith('/account')) {
return context.redirect('/login/');
}
// missing return next()
});
Add return next(); at the end. A linter rule like TypeScript’s noImplicitReturns catches this class of bug.
Changing the response
To add headers, await the response, change it, and return it:
export const onRequest = defineMiddleware(async (_context, next) => {
const response = await next();
response.headers.set('X-Frame-Options', 'DENY');
return response;
});
For a fully different body, return a new Response:
const response = await next();
const html = await response.text();
return new Response(html.replace('{{year}}', String(new Date().getFullYear())), {
status: response.status,
headers: response.headers,
});
A related error
Returning something that isn’t a Response, such as a string or an object, gives “The middleware returned something that is not a Response object.” Wrap it: return new Response('Not allowed', { status: 403 }).
Middleware on static pages
On prerendered pages, middleware runs once at build time. Logic that reads cookies or headers does nothing useful there; it only has an effect on on-demand routes. The hybrid rendering tip explains which pages render when.
The Astro SSR guide has a full middleware example with sequence() and typed locals. Related: locals must not be reassigned. The middleware docs are the reference.
FAQ
Do I need to await next()?
Only if you want to read or modify the response. return next() is enough otherwise.
Can middleware return early for some paths?
Yes. Check context.url.pathname and return next() immediately for paths you don’t handle.