Redirecting Based on Cookies, Headers, or Geolocation

How to send visitors to different routes based on a cookie, a request header, or the location your host reports, using the proxy file and config conditions in the Next.js App Router.

9 min read

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.

typescripttypescript
// 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.

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.

typescripttypescript
// 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.

typescripttypescript
// 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.

typescripttypescript
// 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.

Never make a language redirect permanent

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.

typescripttypescript
// 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.

Do not trap visitors by country

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.

How a conditional redirect loops

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

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.
RunePowered by Rune AI

Frequently Asked Questions

Can I read geolocation from NextRequest?

No. The geo and ip properties were removed from NextRequest in Next.js 15 because those values come from the hosting platform rather than the framework. Read the headers your host sets, or use the helper package your platform provides.

Should a cookie redirect use permanentRedirect or a 308?

No. Anything that depends on the visitor must stay temporary, because a permanent redirect is cached by the browser and would then apply to visitors whose cookie says otherwise.

Why does my conditional redirect loop?

Almost always because the destination also matches the condition. If a visitor without a locale cookie is sent to a page that itself lacks the cookie, the rule fires again. Exclude the destination in the matcher or check the pathname before redirecting.

Is a proxy redirect enough to protect a route?

No. It is an optimistic check that improves the experience for a signed-out visitor. Real authorization has to happen in the page, Server Action, or Route Handler that touches the data.

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.