Description
After upgrading from v0.4.0 to v0.4.1, navigating to /catalog (or any authenticated route) results in a redirect to https://0.0.0.0:3000/catalog instead of the correct external URL. The /login page continues to work fine.
This was introduced by the new preemptive token-refresh mechanism added in v0.4.1.
Environment
- Kubernetes (EKS) with AWS ALB Ingress Controller
- ALB terminates TLS and forwards traffic to pod port 3000
BETTER_AUTH_URL is correctly set to the external URL (e.g. https://mcp.example.com)
- v0.4.0 works correctly; v0.4.1 does not
Steps to Reproduce
- Deploy toolhive-cloud-ui v0.4.1 behind a reverse proxy / load balancer (e.g. AWS ALB, nginx, Traefik)
- Set
BETTER_AUTH_URL to the external URL
- Navigate to
https://<external-domain>/login — works fine
- Navigate to
https://<external-domain>/catalog — browser is redirected to https://0.0.0.0:3000/catalog
Root Cause
The new token-refresh Route Handler in src/app/api/auth/token-refresh/route.ts constructs redirect URLs using request.url:
const redirectResponse = NextResponse.redirect(
new URL(safeRedirect, request.url),
);
Similarly, the open-redirect validation also uses request.nextUrl.origin:
if (resolved.origin === request.nextUrl.origin) {
safeRedirect = resolved.pathname + resolved.search + resolved.hash;
}
In Next.js 16, request.url for Route Handlers is constructed in next-server.ts attachRequestMeta():
const initUrl =
this.fetchHostname && this.port
? `${protocol}://${this.fetchHostname}:${this.port}${req.url}`
: this.nextConfig.experimental.trustHostHeader
? `https://${req.headers.host || 'localhost'}${req.url}`
: req.url
Because the Dockerfile sets ENV HOSTNAME="0.0.0.0", this.fetchHostname is "0.0.0.0" and this.port is 3000. This means the first branch always wins, producing https://0.0.0.0:3000/... regardless of what the ALB forwards in the Host header.
The x-forwarded-proto IS respected (hence https://), but the host is hardcoded to the server bind address.
Suggested Fixes
There are two ways to fix this. Either (or both) would work:
Option A: Use BETTER_AUTH_URL for redirect construction (application-level fix)
The BETTER_AUTH_URL environment variable already represents the correct external origin and is a required configuration value. The token-refresh Route Handler should use it as the base for redirect URL construction instead of request.url:
import { BASE_URL } from "@/lib/auth/constants";
// Before (broken behind reverse proxy)
const redirectResponse = NextResponse.redirect(
new URL(safeRedirect, request.url),
);
// After (uses the configured external URL)
const redirectResponse = NextResponse.redirect(
new URL(safeRedirect, BASE_URL),
);
The same change should apply to the open-redirect origin validation and the /signin fallback redirects in the same file.
Option B: Enable trustHostHeader in next.config.ts (framework-level fix)
Next.js 16 has an experimental config option that tells the server to use the incoming Host header (forwarded by the reverse proxy) instead of the server's bind address when constructing request.url:
// next.config.ts
const nextConfig: NextConfig = {
experimental: {
trustHostHeader: true,
},
// ...existing config
};
This makes request.url resolve correctly behind any reverse proxy (ALB, nginx, Traefik, etc.) without changing application code. This is the more general fix — any Route Handler using request.url for redirects will benefit.
Attempted Workarounds
__NEXT_PRIVATE_ORIGIN env var — does NOT work. In Next.js 16, this env var is only used in action-handler.ts for Server Actions. It is NOT used in attachRequestMeta() which controls request.url for Route Handlers. Additionally, start-server.ts unconditionally overwrites it at startup.
Pin to v0.4.0 — this is the only working workaround for deployers. The token-refresh flow was introduced in v0.4.1, so v0.4.0 does not have this issue.
Affected Versions
- Working: v0.4.0
- Broken: v0.4.1
Related Code
src/app/api/auth/token-refresh/route.ts (new in v0.4.1)
src/lib/api-client.ts (getAuthenticatedClient — triggers the redirect to token-refresh)
src/lib/auth/utils.ts (isTokenNearExpiry — new in v0.4.1)
Description
After upgrading from v0.4.0 to v0.4.1, navigating to
/catalog(or any authenticated route) results in a redirect tohttps://0.0.0.0:3000/cataloginstead of the correct external URL. The/loginpage continues to work fine.This was introduced by the new preemptive token-refresh mechanism added in v0.4.1.
Environment
BETTER_AUTH_URLis correctly set to the external URL (e.g.https://mcp.example.com)Steps to Reproduce
BETTER_AUTH_URLto the external URLhttps://<external-domain>/login— works finehttps://<external-domain>/catalog— browser is redirected tohttps://0.0.0.0:3000/catalogRoot Cause
The new token-refresh Route Handler in
src/app/api/auth/token-refresh/route.tsconstructs redirect URLs usingrequest.url:Similarly, the open-redirect validation also uses
request.nextUrl.origin:In Next.js 16,
request.urlfor Route Handlers is constructed innext-server.tsattachRequestMeta():Because the Dockerfile sets
ENV HOSTNAME="0.0.0.0",this.fetchHostnameis"0.0.0.0"andthis.portis3000. This means the first branch always wins, producinghttps://0.0.0.0:3000/...regardless of what the ALB forwards in theHostheader.The
x-forwarded-protoIS respected (hencehttps://), but the host is hardcoded to the server bind address.Suggested Fixes
There are two ways to fix this. Either (or both) would work:
Option A: Use
BETTER_AUTH_URLfor redirect construction (application-level fix)The
BETTER_AUTH_URLenvironment variable already represents the correct external origin and is a required configuration value. The token-refresh Route Handler should use it as the base for redirect URL construction instead ofrequest.url:The same change should apply to the open-redirect origin validation and the
/signinfallback redirects in the same file.Option B: Enable
trustHostHeaderinnext.config.ts(framework-level fix)Next.js 16 has an experimental config option that tells the server to use the incoming
Hostheader (forwarded by the reverse proxy) instead of the server's bind address when constructingrequest.url:This makes
request.urlresolve correctly behind any reverse proxy (ALB, nginx, Traefik, etc.) without changing application code. This is the more general fix — any Route Handler usingrequest.urlfor redirects will benefit.Attempted Workarounds
__NEXT_PRIVATE_ORIGINenv var — does NOT work. In Next.js 16, this env var is only used inaction-handler.tsfor Server Actions. It is NOT used inattachRequestMeta()which controlsrequest.urlfor Route Handlers. Additionally,start-server.tsunconditionally overwrites it at startup.Pin to v0.4.0 — this is the only working workaround for deployers. The token-refresh flow was introduced in v0.4.1, so v0.4.0 does not have this issue.
Affected Versions
Related Code
src/app/api/auth/token-refresh/route.ts(new in v0.4.1)src/lib/api-client.ts(getAuthenticatedClient— triggers the redirect to token-refresh)src/lib/auth/utils.ts(isTokenNearExpiry— new in v0.4.1)