Redirecting based on cookies, headers, or location means the routing decision depends on the incoming request, so it cannot be a fixed path mapping. In the App Router that work belongs in the proxy file, which runs on the server before a route renders.
The proxy file is the Next.js 16 name for what earlier versions called middleware, and it always runs on the Node.js runtime. Here is a redirect driven by a cookie.
// proxy.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function proxy(request: NextRequest) {
if (!request.cookies.has('onboarded')) {
return NextResponse.redirect(new URL('/welcome', request.url))
}
return NextResponse.next()
}
export const config = { matcher: '/app/:path*' }A visitor without the onboarding cookie is sent to /welcome with a 307 before any page renders. A visitor who has it continues to the route they asked for, because the next response tells Next.js to carry on.
Reading the cookie
The request object exposes cookies through a small helper rather than requiring you to parse a header. It answers three questions: does a cookie exist, what is its value, and what are all of them.
Presence checks use the has method, as above. When the decision depends on the value, read it and compare.
// proxy.ts
export function proxy(request: NextRequest) {
const variant = request.cookies.get('experiment')?.value
if (variant === 'b' && request.nextUrl.pathname === '/pricing') {
return NextResponse.redirect(new URL('/pricing-b', request.url))
}
return NextResponse.next()
}Only visitors whose experiment cookie holds the value b are moved to the variant page, and everyone else sees the original. The optional chaining matters because the get method returns undefined when the cookie is absent.
Note the pathname check alongside the cookie check. Without it, the rule would also fire on the destination, which is how conditional redirects turn into loops.
When the config file is enough
Not every conditional redirect needs code. A rule in the Next.js config can carry conditions, and those run before the proxy file, which makes them cheaper and easier to review.
// next.config.ts
async redirects() {
return [
{
source: '/app/:path*',
missing: [{ type: 'cookie', key: 'onboarded' }],
destination: '/welcome',
permanent: false,
},
]
}This produces the same behavior as the first example without a proxy file at all. The missing array requires the cookie to be absent, and the permanent flag stays false because the outcome depends on the visitor.
Reach for the config file when the condition is presence, absence, or an exact value. Reach for the proxy file when you need to compare, parse, or combine several signals. Configuring redirects covers the condition syntax in full.
Redirecting on a request header
Headers work the same way, through the standard headers object on the request. Language negotiation is the common case, and it needs parsing rather than an exact match.
// proxy.ts
export function proxy(request: NextRequest) {
const preferred = request.headers.get('accept-language')?.split(',')[0] ?? ''
if (preferred.startsWith('fr') && request.nextUrl.pathname === '/') {
return NextResponse.redirect(new URL('/fr', request.url))
}
return NextResponse.next()
}A browser sending a French preference lands on /fr, and everyone else keeps the default homepage. The header value is a weighted list, so taking the first entry is a deliberate simplification rather than full negotiation.
The visitor's language preference can change, and a shared machine may have several. A 308 would freeze the first visitor's language for everyone who follows, so keep these redirects temporary.
Full locale handling usually needs the pathname, the cookie, and the header together. Internationalized routing covers that combination as its own problem.
Geolocation comes from your host
This is where outdated tutorials go wrong. Next.js used to expose geo and ip properties on the request object, and both were removed in version 15 because the framework does not determine them.
Location data now arrives the way it always actually arrived, as request headers set by whatever sits in front of your app. Which headers exist depends entirely on your hosting platform or CDN.
// proxy.ts
export function proxy(request: NextRequest) {
const country = request.headers.get('x-vercel-ip-country')
if (country === 'CA' && request.nextUrl.pathname === '/store') {
return NextResponse.redirect(new URL('/store/ca', request.url))
}
return NextResponse.next()
}That header name is one platform's convention, not a Next.js feature. On another host the header will be named differently or may not exist at all, so check your provider's documentation before relying on it.
Two consequences follow. The header is usually absent in local development, so the branch never fires there, and the value can be wrong for anyone behind a VPN or corporate proxy.
A hard redirect based on location leaves travelers and VPN users with no way back to the site they wanted. Offer a dismissible suggestion instead, or set a cookie the first time so the visitor's own choice wins afterward.
Preventing the loop
Every conditional redirect can fire twice, because the destination is also a request. If the matcher covers the destination and the condition is still true there, the rule runs again and the browser gives up.
The diagram shows the failure: /welcome re-enters the same check because nothing set the cookie in between. The narrow matcher in the first example already avoids this, since /welcome never matches /app/:path*, but a site-wide pattern would walk straight into it.
Two approaches work, and they combine well. Scope the matcher so it never covers the destination path, and check the pathname inside the function as a second guard.
The other reliable fix is to set the cookie on the redirect response itself, so the next request carries it. Debugging redirect loops covers how to identify which rule is repeating.
What these redirects are not for
A conditional redirect improves the experience. It does not enforce anything, because the proxy file sits in front of routes rather than inside them.
- A signed-out visitor sent to a login page can still call your Route Handlers directly.
- A matcher change can remove the check without any test failing.
- A Server Action posts to the route it lives on, so a matcher that skips that path skips the action too.
Authorization has to happen where the data is touched. Securing Server Actions covers the checks that belong inside the server boundary rather than in front of it.
Rune AI
Key Insights
- Conditional redirects belong in the proxy file, which runs before a route renders.
- Simple presence checks can stay in the config file using the has and missing arrays.
- Read cookies with the request cookies helper and headers with the standard headers object.
- Geolocation is not part of NextRequest; the geo and ip properties were removed in Next.js 15.
- Always exclude the destination from the condition, or the redirect will loop.
Frequently Asked Questions
Can I read geolocation from NextRequest?
Should a cookie redirect use permanentRedirect or a 308?
Why does my conditional redirect loop?
Is a proxy redirect enough to protect a route?
Conclusion
A redirect that depends on the visitor needs the request, so it belongs in the proxy file or in a config rule with conditions. Keep these redirects temporary, exclude the destination so the rule cannot fire twice, and treat any location data as something your host provides rather than something Next.js knows.
More in this topic
`generateMetadata` Explained with Real Examples
What generateMetadata does, when it runs, and how to use it for real routes: awaited params, deduplicated data fetching, extending parent metadata, and returning a 404 from metadata.
Canonical URLs in Next.js: `metadataBase`, `alternates.canonical`, and Dynamic Pages
How canonical URLs work in the Next.js App Router: setting metadataBase once, writing alternates.canonical per route, handling dynamic segments, and what happens when the base URL is missing.
Open Graph and Twitter Card Metadata in Next.js
How to write Open Graph and Twitter card metadata in the Next.js App Router: the openGraph and twitter fields, automatic card defaults, article tags, and image merge rules.