fix: strip internal container port from redirect URLs
Deploy to VPS / deploy (push) Has been cancelled
Deploy to VPS / deploy (push) Has been cancelled
The site was redirecting / -> https://rf-flux.com:3000/en, where :3000 is the container's internal port (only "expose"d, not published) — so the browser saw ERR_CONNECTION_REFUSED. Root cause: when running behind Nginx in standalone mode, Next.js (via next-intl in this case) can build absolute redirect URLs that leak the container's internal PORT/HOSTNAME env into the Location header. TWO LAYERS OF DEFENCE 1. Nginx (nginx/conf.d/flux.conf) - Adds X-Forwarded-Host + X-Forwarded-Port so the upstream knows the public port (443) and host - proxy_redirect rewrites any Location header that still slips through with :3000 back to the public https://$host 2. Middleware (src/proxy.ts) - sanitizeRedirectLocation() runs after handleI18nRouting and scrubs Location headers that point at internal hostnames (app / localhost / 0.0.0.0) or the container port :3000, replacing them with the public host derived from x-forwarded-host / host header. Either layer alone would fix the immediate symptom; together they also prevent the same class of bug from showing up in any future redirect path.
This commit is contained in:
+53
-2
@@ -51,9 +51,60 @@ export async function proxy(request: NextRequest) {
|
||||
// --------------------------------------------------------
|
||||
// ZONA VERDE: WEB PÚBLICA (Motor de Traducciones i18n)
|
||||
// --------------------------------------------------------
|
||||
// Si el usuario no va al CMS, le pasamos el control a next-intl
|
||||
// Si el usuario no va al CMS, le pasamos el control a next-intl
|
||||
// para que gestione los prefijos /en/, /it/, /vec/, etc.
|
||||
return handleI18nRouting(request);
|
||||
const response = handleI18nRouting(request);
|
||||
|
||||
// Belt-and-suspenders fix for the "redirect to :3000" bug behind Nginx.
|
||||
// next-intl can occasionally compose absolute redirect URLs using the
|
||||
// container's internal port (PORT=3000 from the Dockerfile) instead of
|
||||
// the public-facing host. We sanitise any Location header before sending
|
||||
// it back to the browser.
|
||||
const location = response.headers.get('Location');
|
||||
if (location) {
|
||||
const cleaned = sanitizeRedirectLocation(location, request);
|
||||
if (cleaned !== location) {
|
||||
response.headers.set('Location', cleaned);
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
function sanitizeRedirectLocation(location: string, request: NextRequest): string {
|
||||
try {
|
||||
const forwardedHost =
|
||||
request.headers.get('x-forwarded-host') || request.headers.get('host');
|
||||
const forwardedProto =
|
||||
request.headers.get('x-forwarded-proto') ||
|
||||
(request.nextUrl.protocol ? request.nextUrl.protocol.replace(':', '') : 'https');
|
||||
|
||||
// Relative redirect — leave alone.
|
||||
if (!/^https?:\/\//i.test(location)) return location;
|
||||
|
||||
const url = new URL(location);
|
||||
const internalPorts = new Set(['3000', '80']);
|
||||
const isInternalHost = url.hostname === '0.0.0.0' || url.hostname === 'localhost' || url.hostname === 'app';
|
||||
const isInternalPort = url.port && internalPorts.has(url.port);
|
||||
|
||||
if (forwardedHost && (isInternalHost || isInternalPort)) {
|
||||
const [hostOnly] = forwardedHost.split(':');
|
||||
url.hostname = hostOnly;
|
||||
url.port = '';
|
||||
url.protocol = `${forwardedProto}:`;
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
// Same host, just a stray internal port — strip it.
|
||||
if (url.port === '3000') {
|
||||
url.port = '';
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
return location;
|
||||
} catch {
|
||||
return location;
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user