How to Fix: redirect action to protected routes(protect with middleware) without authorization will result in URL change
The bug is not that the redirect fails; it is that the browser URL changes to the protected destination before middleware finishes denying access, leaving users on the login page while the address bar still shows the blocked route.
Table of Contents
Problem Overview
In this Next.js issue, a user attempts to navigate with a redirect action to a protected route after authentication state has been removed, such as after deleting cookies. The target page is guarded by middleware, so the request is correctly blocked and the user is sent back to an authentication page or public page.
However, the visible problem is that the URL changes first. The app may render the redirected fallback content, but the address bar still reflects the protected route. This creates confusing behavior, especially in the App Router, because the user appears to be on a page they are not actually authorized to access.
If you are reproducing this from the linked repository, the sequence is usually:
- Start the application in development mode.
- Delete the auth cookie.
- Trigger a server action or redirect that points to a protected route.
- Observe that middleware blocks access, but the browser URL still updates to the protected path.
This is typically a mismatch between client-side navigation expectations and server-side auth enforcement.
Understanding the Root Cause
The root cause is tied to how Next.js navigation, server actions, and middleware interact.
When a redirect is triggered from a server action, Next.js may instruct the client router to navigate to the new route. That route change can update the browser history and address bar before the final protected response is fully resolved through middleware. If middleware then detects that the request lacks authorization, it returns a redirect or rewrite to a safe page. The result is a UX inconsistency: the app content reflects the fallback route, but the URL may already have been pushed to the blocked one.
Technically, the issue happens because these layers do different jobs:
- Server actions can issue redirects after mutations or form submissions.
- Client router state may optimistically apply the navigation.
- Middleware runs on the incoming request and enforces access rules.
- Browser history may already contain the attempted protected URL.
In other words, middleware is not the best place to repair a bad post-action navigation target. Middleware can block unauthorized access, but if the redirect destination itself is invalid for the current session, the cleanest fix is to avoid redirecting there in the first place.
This is why the most reliable solution is to perform the auth check before issuing the redirect, especially inside the server action or on the server boundary where the redirect decision is made.
Step-by-Step Solution
The fix is to make your redirect destination authorization-aware. Instead of always redirecting to a protected route and letting middleware reject it later, check the session or cookie first and redirect to a valid page immediately.
1. Keep middleware for route protection
Middleware should still guard private routes, but it should be your safety net, not the primary way to correct an invalid redirect flow.
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const token = request.cookies.get('token')?.value
const isProtectedRoute = request.nextUrl.pathname.startsWith('/dashboard')
if (isProtectedRoute && !token) {
const loginUrl = new URL('/login', request.url)
loginUrl.searchParams.set('from', request.nextUrl.pathname)
return NextResponse.redirect(loginUrl)
}
return NextResponse.next()
}
export const config = {
matcher: ['/dashboard/:path*']
}
This protects direct requests and manual URL entry, which is still necessary.
2. Fix the server action redirect logic
If a server action may redirect to a protected route, validate authentication first.
'use server'
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
export async function continueToDashboard() {
const token = cookies().get('token')?.value
if (!token) {
redirect('/login')
}
redirect('/dashboard')
}
This prevents the app from attempting a navigation that middleware will immediately reject.
3. If the action deletes auth state, never redirect to a protected route afterward
This is one of the most common causes of the bug. If you remove cookies and then redirect to a private page, you are creating an invalid state transition.
'use server'
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
export async function logout() {
cookies().delete('token')
redirect('/login')
}
After logout or cookie deletion, always redirect to a public route such as /login, /, or a session-expired page.
4. Avoid client-side push to protected routes when auth is stale
If you are navigating from a client component, do not call router.push('/dashboard') when the session is missing or has just been invalidated.
'use client'
import { useRouter } from 'next/navigation'
export function Actions({ isAuthenticated }: { isAuthenticated: boolean }) {
const router = useRouter()
const handleClick = () => {
if (!isAuthenticated) {
router.replace('/login')
return
}
router.push('/dashboard')
}
return <button onClick={handleClick}>Go</button>
}
Using replace instead of push for unauthorized fallbacks also avoids polluting browser history with a route the user should never see.
5. Prefer server-derived auth over stale client state
A local auth flag in React state can be outdated right after cookie deletion. If your app depends on cookies, session headers, or server validation, derive the redirect target on the server whenever possible.
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
export default function ProtectedEntryPage() {
const token = cookies().get('token')?.value
if (!token) {
redirect('/login')
}
redirect('/dashboard')
}
This removes timing issues between client state updates and middleware execution.
6. Use a dedicated public handoff route when flows are complex
If your action has multiple possible outcomes, redirect first to a public route that decides where to go next based on current auth state.
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
export default function ContinuePage() {
const token = cookies().get('token')?.value
if (!token) {
redirect('/login')
}
redirect('/dashboard')
}
This pattern is useful when forms, server actions, and expired sessions can overlap.
7. Test in development and production
Next.js routing behavior can feel more noticeable in development because of extra rendering layers and debugging behavior. Always verify the final flow in a production build:
npm run build
npm run start
If the issue appears worse in development, that does not change the architectural fix: never redirect users to a route you already know they cannot access.
Common Edge Cases
- Expired session during a server action: The action starts with a valid UI state, but the cookie expires before the redirect. Solve this by checking auth inside the action itself, not only in the component.
- Client state says logged in, server says logged out: This mismatch is common after manual cookie deletion. Always trust the server-side source of truth.
- Using rewrite instead of redirect in middleware: A rewrite can preserve the original URL intentionally, which may make this problem look worse. For auth protection, use a real redirect when the URL should change.
- Browser back button behavior: If you used
pushbefore auth failure, the blocked URL may stay in history. Prefer replace when sending unauthorized users to login. - Prefetched protected routes: If a route was prefetched while authenticated and then the cookie disappears, later navigation may feel inconsistent. Revalidate auth on the server at navigation time.
- Mixed public/private layouts: If a parent layout caches assumptions about auth, users can briefly see stale UI. Keep protected data behind server checks and avoid relying only on cached client context.
FAQ
Why does the URL change even though middleware blocks the route?
Because the navigation attempt is initiated before middleware finishes enforcing authorization. The client router or redirect response can update browser history first, and middleware then redirects the actual request elsewhere.
Should I remove middleware and handle auth only in server actions?
No. Keep middleware to protect direct access to private routes. The fix is to also validate auth before issuing redirects from server actions or client navigation, so users are never sent toward an invalid destination.
What is the safest redirect target after deleting auth cookies?
A public route such as login, the home page, or a session-expired page. After logout or cookie deletion, redirecting to a protected route guarantees middleware will have to undo the navigation.
Recommended Fix Summary
To solve this issue cleanly, keep middleware as the guardrail but move redirect decisions closer to the auth source of truth. Check cookies or session state before redirecting, send logged-out users only to public routes, and use router.replace for unauthorized fallbacks when navigating on the client. That prevents the protected URL from appearing in the address bar and produces a consistent, correct auth flow.
For the reproduction linked in the issue, the practical fix is simple: after deleting the cookie, do not redirect to the protected route at all. Redirect directly to a public page, and let middleware remain as backup protection rather than the mechanism that repairs a bad redirect target.