Skip to main content
All posts
Frontend6 min read

Nuxt Middleware Runs Twice and Breaks Redirects on the First Load

A route guard that sent signed out visitors to the login page worked perfectly on client navigation and bounced signed in users on a hard refresh. The middleware was doing its job twice, in two places, with two different views of the session.

Portrait of Benjamin Fazli, the author of bfzli.com

Benjamin Fazli

Principal EngineerSkopje, North Macedonia

Abstract glossy grid pattern with fluid intersections

The symptom

An authenticated area used a single route middleware. Navigating inside the app behaved correctly. Refreshing the page while signed in redirected to the login screen, and a second later the app corrected itself and navigated back. Users described it as a flash, which is a generous word for it.

Why it happens

Route middleware in Nuxt runs on the server during the initial request and again on the client during hydration. That is intentional, and most of the time you never notice. It becomes visible when the two environments do not have the same information.

My session came from a composable that read a token out of local storage. On the server there is no local storage, so the composable returned nothing, the middleware concluded the visitor was signed out, and the redirect went out with the server response. On the client the token was present, the state corrected itself, and the router navigated back. Two runs, two answers, one visible flash.

The fix

Read the session from a cookie so both environments see the same value:

ts
export default defineNuxtRouteMiddleware((to) => {
    const token = useCookie('session')

    if (!token.value && to.path.startsWith('/app')) {
        return navigateTo('/login')
    }
})

useCookie is readable on the server from the request headers and on the client from the document, so the middleware reaches the same conclusion in both passes and the redirect either happens once or not at all.

Two related patterns worth knowing:

  • If a guard genuinely can only run in the browser, say so explicitly rather than hoping. if (import.meta.server) return at the top of the middleware is clearer than a subtle mismatch, though it does mean the server sends protected markup before the client redirects.
  • Do not fetch inside middleware without caching the result. Middleware runs on every navigation, and an uncached call turns a route change into a network round trip.

The general rule

Anything that decides what a user sees has to be able to reach the same decision on the server and in the browser. Local storage, window, and navigator fail that test by definition. Cookies and request headers pass it. Choose the storage mechanism based on where the decision needs to be made, not on which API you reached for first.

Every framework with server rendering has this class of bug. Nuxt calls it middleware, Next calls it a hydration mismatch, and the underlying cause is identical: state that only exists on one side of the render.