How to Fix: Nextjs rewrites replaces x-forwarded-host in production
Table of Contents
Encountering situations where Next.js rewrites appear to replace or ignore the X-Forwarded-Host header in a production environment behind a proxy like Nginx is a common and frustrating bug. This often leads to incorrect absolute URLs, broken canonical tags, and misdirected API calls, as your Next.js application mistakenly believes it’s running on an internal hostname (e.g., localhost:3000 or an internal IP) rather than its public-facing domain.
Understanding the Root Cause
When a client request reaches your Next.js application through an external proxy (like Nginx, Apache, or a cloud load balancer), the proxy typically acts as an intermediary. The client’s original host header (e.g., www.example.com) is often preserved and passed down to the upstream server (your Next.js app) via the X-Forwarded-Host header. Simultaneously, the proxy establishes its own connection to your Next.js application, setting the standard Host header to the internal address it uses to reach your app (e.g., localhost:3000).
Next.js, by default, often relies on the standard Host header for internal routing decisions and for constructing absolute URLs (e.g., in server-side data fetching, redirects, or SEO metadata). The issue arises because when Next.js processes a rewrite, it may re-evaluate the request context. If not explicitly told otherwise, or if the proxy configuration is slightly off, Next.js might prioritize the internal Host header over the X-Forwarded-Host for certain operations or when generating URLs after the rewrite has taken effect.
Specifically, the problem isn’t necessarily that the rewrite itself is explicitly modifying X-Forwarded-Host (it typically operates on paths and internal routing). Instead, it’s that after the rewrite, Next.js’s subsequent processing or URL generation logic incorrectly defaults to the internal Host header, thereby effectively ‘replacing’ the original public host information that was available in X-Forwarded-Host.
Step-by-Step Solution
The most robust solution involves a combination of correct proxy configuration and leveraging Next.js Middleware to ensure the application always has the correct public host context.
1. Verify/Update Your Proxy Configuration (Nginx Example)
Ensure your Nginx configuration correctly passes all necessary X-Forwarded-* headers, especially X-Forwarded-Host, and ideally, sets the primary Host header to the original client host.
In your Nginx server block for proxying to Next.js:
server {
listen 80;
server_name your-domain.com www.your-domain.com;
location / {
proxy_pass http://localhost:3000; # Or your Next.js internal address
proxy_set_header Host $http_host; # CRITICAL: Pass original Host header
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $http_host; # Ensure X-Forwarded-Host is also correct
proxy_set_header X-Forwarded-Port $server_port;
# Optional: For WebSocket support if your Next.js app uses it
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
Explanation:
proxy_set_header Host $http_host;: This is crucial. It tells Nginx to set theHostheader sent to your Next.js application to the originalHostheader received from the client. Many setups only haveproxy_set_header Host $host;which might resolve to Nginx’s own hostname, not the client’s.$http_hostrefers to the original client’s host header.proxy_set_header X-Forwarded-Host $http_host;: Ensures that even if other systems rely onX-Forwarded-Host, it contains the correct value.
2. Implement Next.js Middleware to Normalize the Host Header
While Nginx configuration helps, relying solely on it can be brittle. Next.js Middleware provides an excellent way to guarantee that your application always knows its public host by explicitly setting the Host header in the incoming request based on X-Forwarded-Host if it exists.
Create a file named middleware.ts (or middleware.js) in the root of your project:
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const xForwardedHost = request.headers.get('x-forwarded-host');
const xForwardedProto = request.headers.get('x-forwarded-proto');
// If X-Forwarded-Host and X-Forwarded-Proto are present, rewrite the Host header
// This ensures Next.js internal URL generation uses the correct public host.
if (xForwardedHost && xForwardedProto) {
const url = request.nextUrl.clone();
// Construct the full public URL base
url.protocol = xForwardedProto + ':';
url.host = xForwardedHost;
// Create a new request with the updated URL. This doesn't actually redirect,
// but rather modifies the request object's properties for downstream processing.
const newRequest = new NextRequest(url.toString(), {
headers: request.headers,
// The 'Host' header should be set to the X-Forwarded-Host for consistency.
// We can't directly modify request.headers, so we clone them and add/override.
// However, cloning the URL itself usually handles the Host implicitly for nextUrl.
// To be explicit for other server-side checks, we can manually set it in the response.
});
// For clarity, though nextUrl.host is usually sufficient, you might want to ensure
// the 'Host' header itself reflects the external host for server-side APIs.
// Note: You can't directly modify request.headers and expect it to propagate
// back into the `request` object for pages/APIs. The best approach is to ensure
// `nextUrl` itself is accurate and use it.
// If you need to make a response header for some reason (e.g., debugging), you would do:
// const response = NextResponse.next();
// response.headers.set('X-Correct-Host-From-Middleware', xForwardedHost);
// return response;
// The primary goal here is to ensure `request.nextUrl` is accurate.
// By cloning `request.nextUrl` and updating its host/protocol, subsequent access
// to `request.nextUrl.origin` or `request.nextUrl.host` will reflect the correct public URL.
return NextResponse.rewrite(url);
}
return NextResponse.next();
}
export const config = {
matcher: '/((?!api|_next/static|_next/image|favicon.ico).*)',
};
Explanation:
- The middleware checks for the presence of
X-Forwarded-HostandX-Forwarded-Proto. - If found, it clones the
request.nextUrland updates itsprotocolandhostproperties to reflect the original public URL. NextResponse.rewrite(url)doesn’t perform an HTTP redirect; it internally rewrites the request URL within Next.js. This ensures that when your page components or API routes accessreq.urlorrequest.nextUrl, they already contain the correct public host information.- The
matcherconfig ensures this middleware runs for all paths except static assets, API routes (which often handle their own host logic), and internal Next.js files. You might adjust it based on your specific needs.
3. Accessing the Host in Server-Side Code
With the proxy and middleware correctly configured, your Next.js server-side code (getServerSideProps, API Routes, etc.) can now reliably access the public host. While request.headers.host *should* now be accurate due to Nginx, it’s safer to rely on request.nextUrl.host or request.nextUrl.origin within middleware and server components/pages, or continue to use `x-forwarded-host` if it fits your specific logic.
Example in getServerSideProps or API Route:
// pages/my-page.tsx or api/my-api.ts
import { GetServerSideProps, NextApiRequest, NextApiResponse } from 'next';
// For Pages (getServerSideProps)
export const getServerSideProps: GetServerSideProps = async ({ req }) => {
const currentHost = req.headers.host; // Should now be the correct public host due to Nginx/middleware
const xForwardedHost = req.headers['x-forwarded-host']; // Still good to have for debugging/fallback
const publicOrigin = `https://${currentHost}`;
console.log('Host from req.headers:', currentHost);
console.log('X-Forwarded-Host:', xForwardedHost);
console.log('Public Origin:', publicOrigin);
return {
props: {
data: `Hello from ${publicOrigin}`,
canonicalUrl: `${publicOrigin}${req.url}`,
},
};
};
// For API Routes
export default function handler(req: NextApiRequest, res: NextApiResponse) {
const currentHost = req.headers.host; // Should be accurate
const xForwardedHost = req.headers['x-forwarded-host'];
const publicOrigin = `https://${currentHost}`;
console.log('API Host from req.headers:', currentHost);
console.log('API X-Forwarded-Host:', xForwardedHost);
console.log('API Public Origin:', publicOrigin);
res.status(200).json({ message: `API accessed from ${publicOrigin}` });
}
Common Edge Cases
-
Multiple Proxies/Load Balancers: If you have multiple layers of proxies (e.g., Cloudflare > AWS ELB > Nginx > Next.js), ensure each layer correctly propagates the
X-Forwarded-*headers. The outermost proxy will set the initialX-Forwarded-For, and subsequent proxies should append to it. -
Rewrites to External URLs: If your
rewritesconfiguration points to an external URL (e.g., a different microservice), the host context might change again. Ensure that any subsequent redirects or internal links from that external service correctly point back to your public Next.js host. -
HTTPS Configuration: Always ensure your proxy handles HTTPS termination and sets
X-Forwarded-Proto: https. Your Next.js app should then correctly build absolute URLs withhttps://. -
Strict Security Policies: If you have very strict Content Security Policy (CSP) or other security headers, ensure that absolute URLs generated from the corrected host don’t inadvertently violate these policies.
-
Next.js Version Differences: While this solution is generally applicable, very old Next.js versions might behave differently with middleware or request header processing. Always test thoroughly after upgrading.
FAQ
- Q: Why is
X-Forwarded-Hostimportant, and why can’t I just use theHostheader? - A:
X-Forwarded-Hostis a de facto standard header used by proxies to communicate the originalHostheader from the client to the backend server. The standardHostheader received by your Next.js app might reflect the internal IP or hostname of the proxy-to-app connection, not the public domain. Relying solely onHostcan lead to incorrect absolute URLs (e.g.,http://localhost:3000/my-pageinstead ofhttps://www.example.com/my-page), breaking various functionalities and SEO. - Q: Can I solve this solely with
next.config.js? - A: It’s challenging.
next.config.jsrewritesprimarily focus on path-based routing. While you could technically try to manipulate headers in a custom server, or if your rewrite is to an external resource with a different domain, the most reliable and idiomatic Next.js way to ensure the correct host context for *all* internal Next.js operations is through Middleware, combined with proper proxy configuration. - Q: Does this solution affect
router.asPathorrouter.pathname? - A: No, not directly.
router.asPathandrouter.pathnamein client-side code reflect the URL path as seen by the browser or the routing logic within Next.js. This solution primarily addresses the host part of the URL, especially in server-side contexts where Next.js needs to construct absolute URLs (e.g., for canonical tags, server-side redirects, or API calls fromgetServerSideProps).