In a Nuxt 4 auth route middleware, navigateTo('/login') is called but not returned. Why do signed-out users still reach /account, and what does returning it change?
answer
- the return value decides
- in middleware navigateTo only builds a target
- undefined means carry on
- server redirect defaults to 302
- abortNavigation stops instead
basics
~20 sInside Nuxt route middleware, navigateTo only produces a redirect target; the middleware's return value decides the navigation. Unreturned, the middleware returns undefined and /account renders; returned, Nuxt redirects to /login, as a 302 during the server render.
solid answer
~40 sNuxt route middleware talks to the router only through its return value. While middleware is running, `navigateTo('/login')` does not navigate anywhere: on the client it hands back the target location, and on the server it returns the target and arms an `afterEach` hook that writes the redirect response only if navigation actually ends at `/login`. If the middleware drops that value, it returns `undefined`, which means continue, so `/account` is server-rendered with a 200, and the client run during hydration lets it through as well. Returning it makes Nuxt pass the location to Vue Router as a redirect; on the server the response becomes a `302` with a `Location` header, adjustable with `redirectCode`. The other outcomes are returning nothing to continue and `return abortNavigation()` to stop.
code
ts · 11 lines// app/middleware/auth.ts
export default defineNuxtRouteMiddleware((to) => {
const session = useCookie('session')
if (!session.value) {
// Without `return`, this line builds a target and
// the navigation to /account carries on.
return navigateTo({ path: '/login', query: { next: to.fullPath } })
}
// no return value: continue to the page
})go deeper
Recall that Nuxt route middleware must return navigateTo(...) to redirect, and that returning nothing lets the navigation continue.
Explain the return-value protocol: undefined continues, a location redirects, false aborts, and how redirectCode and replace change the redirect.
Trace the bug on both sides: the server arms an afterEach hook and returns the target, the client returns it at once, so a dropped return silently serves the protected page.
Treat route middleware as a UX gate, not the security boundary: data needs server-side checks too, and redirect policy should be tested for loops and status codes.
## Middleware speaks through its return value A Nuxt 4 **route middleware** is a function wrapped in `defineNuxtRouteMiddleware((to, from) => ...)`, kept in `app/middleware/` or written inline in `definePageMeta`. Nuxt runs it inside its router `beforeEach` hook for every navigation it applies to. Unlike a Vue Router 3 guard, it receives **no `next()` callback**: its only way to influence the navigation is **what it returns**. | Return value | Effect | |---|---| | nothing, or `undefined` | continue to the next middleware, then render the page | | `navigateTo('/login')`, returned | redirect; during the server render a `302` by default | | `navigateTo('/login', { redirectCode: 301 })`, returned | redirect with a `301` during the server render | | `abortNavigation()` | returns `false`: stop the navigation | | `abortNavigation(error)` | throws the error, rejecting the navigation | The Nuxt docs recommend these helpers over other raw values Vue Router would also accept, which may break in future releases. ## What navigateTo does inside middleware `navigateTo` checks whether middleware is being processed and behaves differently when it is: 1. **Client, inside middleware:** it returns the target location at once, with `replace: true` folded in if you asked for it. It does not call `router.push`. 2. **Server, inside middleware:** it returns the target location and registers a router `afterEach` hook. When navigation finishes *at that target*, the hook writes the response: status `302` (or the `redirectCode`), a `Location` header, and a small meta-refresh body. 3. **Outside middleware**, in an event handler or component setup, it really navigates by calling `router.push` (or `router.replace`), which is why the docs say to always `await` or return it. The server hook waits until the end on purpose. A later middleware may redirect somewhere else, and only the final destination should produce a response. ## Tracing the dropped return The `auth` middleware calls `navigateTo('/login')` and falls off the end: - **Server render of `/account`:** `navigateTo` arms its hook and returns the `/login` location, but the value is discarded, so the middleware returns `undefined`. Navigation continues and ends at `/account`. The hook compares the final path with `/login`, finds no match, and writes nothing. The visitor receives the account page's HTML with a `200`. - **Hydration in the browser:** for a server-rendered first load, middleware runs again on the client. `navigateTo` returns `/login` early, the value is dropped again, and the account page stays. - **Later client-side navigations** repeat the client behaviour. Nothing throws and nothing logs; the redirect simply never happens. Adding `return` fixes all three cases: Vue Router receives a location from the guard and redirects to `/login`, and on the server the hook now fires because navigation ends where it was pointed. ## Redirect details worth knowing - **Status:** `302` by default, `redirectCode: 301` for permanent moves. Client-side redirects involve no HTTP response, so there is no status. - **History:** `replace: true` replaces the current history entry instead of pushing a new one. - **External targets:** a URL with a protocol or host throws `NUXT_E2001` unless you pass `external: true`. - **Route objects** work too: `navigateTo({ path: '/login', query: { next: to.fullPath } })` carries the return URL along. ## Hardening the auth middleware - **Read `to`, not `useRoute()`.** Middleware runs before the navigation is confirmed, so there is no current route to read, and Nuxt warns when `useRoute()` is called inside middleware. - **Make the session readable on both sides.** A cookie works during the server render and in the browser; `localStorage` exists only in the browser. - **Avoid loops.** If this check ever becomes global, it must let `/login` through, or every redirect to `/login` triggers another one. - **Prefer a redirect to `abortNavigation()` for guests.** During the server render, and on a client-only first load, a `false` result becomes a 404 error page, the wrong message for someone who only needs to sign in. - **Do not treat it as the security boundary.** Route middleware decides what the Vue app shows; data served by `/api` routes needs its own server-side check, because route middleware never runs for them.
- What does return abortNavigation() do in the same middleware, on the server and on the client?It returns `false`. During a client-side navigation Vue Router cancels it, and the user stays on the current page. During the server render, and on a client-only first load, Nuxt turns `false` into a 404 Page Not Found error page, so it is a poor substitute for a login redirect. `abortNavigation(error)` throws the error instead, and a fatal error shows the error page.
- Why does navigateTo inside middleware not write the redirect immediately on the server?Several middleware can run for one navigation, and a later one may redirect somewhere else. So on the server `navigateTo` returns the target and registers an `afterEach` hook that writes the redirect only if the final route matches that target. That is also why a dropped return is silent: navigation finishes at `/account`, the hook's condition fails, and nothing is written.
- How does navigateTo behave when the middleware must send the visitor to another site?`navigateTo` rejects external URLs by default and throws `NUXT_E2001`; passing `{ external: true }` allows them. During the server render that becomes a redirect response to the external location. In the browser Nuxt sets `location.href`, or calls `location.replace` with `replace: true`, and inside middleware on a hydrated app it returns `false` to abort the in-app navigation.
Inside middleware, navigateTo is like filling in a transfer slip at a checkpoint: writing the slip changes nothing until you hand it to the guard. Returning the slip is what makes the guard send the traveller to /login instead of waving them through to /account.
saying these in an interview costs you the question
- Calling navigateTo inside middleware redirects immediately, returned or not
- Nuxt middleware should call next('/login') like a Vue Router 3 guard
- abortNavigation() is the right way to send guests to the login page
- A redirect from middleware during the server render always uses 301
- Returning nothing from a route middleware blocks the navigation
- useRoute() inside middleware gives the route being navigated to