Skip to content

fix(control-plane): pin next to 16.2.5 to fix standalone i18n redirect loop (#1926) - #1928

Merged
nicoloboschi merged 1 commit into
mainfrom
fix/cp-pin-next-16-2-5
Jun 2, 2026
Merged

nicoloboschi merged 1 commit into
mainfrom
fix/cp-pin-next-16-2-5

Conversation

@nicoloboschi

Copy link
Copy Markdown
Collaborator

Summary

Fixes #1926 — every control-plane page route 307-redirect-loops (ERR_TOO_MANY_REDIRECTS) under the standalone production server.

Root cause: a Next.js regression in 16.2.6. With the standalone default HOSTNAME=0.0.0.0, next-intl's localePrefix: "as-needed" locale rewrite is emitted as an absolute localhost URL and resolved as cross-origin, so the rewrite leaks to the client as a 307 to the original path → loop.

Bisected (raw standalone, HOSTNAME=0.0.0.0, identical CP code):

next i18n page routes
16.1.7 ✅ 200 (relative rewrite)
16.2.5 ✅ 200 (relative rewrite)
16.2.6 ❌ 307 loop (absolute rewrite)
16.2.7 ❌ 307 loop

next dev is not affected — only the standalone build, which is why it surfaced only when building/shipping the production image. This is also why 0.7.1 images that resolved 16.2.5 worked.

Change

  • Pin next to 16.2.5 (exact) in hindsight-control-plane.
  • Add a root overrides.next: "16.2.5" so next-intl's peer dedupes to the same single version (a 16.2.5/16.2.6 split fails the CP typecheck with a NextRequest mismatch).
  • Lockfile updated minimally (next-family 16.2.6 → 16.2.5, single hoisted, all platform binaries preserved).

⚠️ Temporary pin

16.2.6 is a security release (multiple high-severity middleware/proxy bypass fixes + an SSRF). Pinning to 16.2.5 re-opens those, so this should be treated as a stopgap until the regression is fixed upstream and we can move back to a patched version.

Upstream issue filed for guidance: vercel/next.js#94342

Verification

  • npm ci at root → single deduped next@16.2.5, consistent lockfile.
  • npm run build --workspace=hindsight-control-plane → compiles + typechecks.
  • Standalone HOSTNAME=0.0.0.0: /login, /banks/abc, /es/login → 200 (was 307 loop).

Note: the issue's manual repro uses HOSTNAME=127.0.0.1, which is a stricter case that still loops on 16.2.5 — but it is not a deployment path (all launchers default to 0.0.0.0). Verify with HOSTNAME=0.0.0.0.

…t loop (#1926)

next 16.2.6 regressed how the standalone server resolves next-intl locale
rewrites. With the standalone default HOSTNAME=0.0.0.0, the i18n rewrite is
emitted as an absolute localhost URL and treated as cross-origin, so every page
route returns a 307 to itself (ERR_TOO_MANY_REDIRECTS). Bisected: 16.2.5 serves
200 with a relative rewrite; 16.2.6 and 16.2.7 loop. next dev is unaffected.

Pin next to 16.2.5 (exact) and add a root override so next-intl's peer dedupes
to the same single version — a 16.2.5/16.2.6 split fails the control-plane
typecheck. The Docker image build resolves the exact pin; CI `npm ci` installs
the pinned lockfile (single hoisted next@16.2.5, all platform binaries kept).

Temporary: 16.2.6 is a security release, so we should return to a patched
version once the regression is fixed upstream. Tracking: vercel/next.js#94342.
@nicoloboschi
nicoloboschi merged commit 201f5d7 into main Jun 2, 2026
196 of 207 checks passed
nicoloboschi added a commit that referenced this pull request Jun 2, 2026
)

Reverts the temporary `next` pin from #1928. Deeper investigation showed the
control-plane redirect loop (#1926) is NOT a 16.2.6 regression: it reproduces
identically on 16.2.5 and 16.2.6, and is triggered specifically by binding the
standalone server to HOSTNAME=127.0.0.1 (Next normalizes 127.0.0.1 -> localhost
in the proxy request URL but keeps 127.0.0.1 in the router's initUrl, so the
next-intl locale rewrite looks cross-origin and leaks as a 307 loop).

The production launchers (docker start-all.sh, bin/cli.js) bind HOSTNAME=0.0.0.0,
which serves 200 on every version, so the pin neither fixed #1926's repro nor was
needed for production. Restoring ^16.2.6 brings back the 16.2.6 security fixes
(proxy-bypass + SSRF). The 127.0.0.1-binding quirk is unrelated to the version.

Verified: npm ci -> single next@16.2.7; control-plane build typechecks; standalone
on HOSTNAME=0.0.0.0 serves /login, /banks/*, /es/login as 200.
nicoloboschi added a commit that referenced this pull request Sep 15, 2026
)

* fix(control-plane): serve the UI when bound to a loopback address (#1926)

A literal loopback HINDSIGHT_CP_HOSTNAME makes the control plane 307 every
page to itself. Next.js standalone uses HOSTNAME both to bind and as the
origin its next-intl locale rewrite is checked against; Next normalizes
127.0.0.1 -> localhost on one side of that comparison but keeps the literal
on the other, so the rewrite reads as cross-origin and leaks out instead of
being applied internally.

What makes it expensive is that nothing else notices. The bind is correct,
/health answers, the whole REST API works — only the browser surface is
dead, so container health checks and smoke tests stay green while the UI is
completely unreachable.

Bind as `localhost` instead when a loopback literal is requested, and pin
Node's DNS result order to the family that was asked for. The pin is not
incidental: `localhost` resolves through /etc/hosts, and Docker's generated
hosts file maps `::1 localhost`, so substituting the name alone would move a
requested IPv4 bind onto IPv6. With the pin the effective bind is unchanged
and only the spelling differs, which is the whole point — the operator gets
the address they asked for and a UI that loads.

Measured on 0.10.0 (Next 16.3.5), control-plane-only containers, one
variable at a time:

  HINDSIGHT_CP_HOSTNAME    bind        GET /dashboard
  127.0.0.1                127.0.0.1   307 -> itself (loop)
  ::1                      ::1         500
  localhost                ::1         200   <- family changed
  localhost + ipv4first    127.0.0.1   200
  localhost + ipv6first    ::1         200

Verified end to end by mounting the patched script over /app/start-all.sh in
the published 0.10.0 image: 127.0.0.1 and ::1 both now bind their own family
and return 200, and 0.0.0.0 is untouched and silent.

Upgrading Next does not fix this. #1928 pinned 16.2.5 for this issue and
#1934 reverted the pin after re-diagnosing, concluding production was
unaffected — which holds for the 0.0.0.0 default and not for a deliberately
loopback-only deployment — and deferred the loopback case to a follow-up.
That follow-up has not landed; 16.3.5 still loops.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(control-plane): apply the loopback bind fix to the npm CLI too

bin/cli.js passed HOSTNAME / -H 127.0.0.1 straight to the standalone server,
so npx users binding to loopback still got the #1926 redirect loop. Mirror
resolve_cp_hostname: bind as localhost and pin --dns-result-order to the
requested family, appending to any existing NODE_OPTIONS. Verified in the
0.10.0 image: 127.0.0.1 and ::1 now serve /dashboard 200 on the same family.

---------

Co-authored-by: snevesgraca-nwmo <257406126+snevesgraca-nwmo@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Nicolò Boschi <boschi1997@gmail.com>
cdbartholomew added a commit that referenced this pull request Oct 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Control plane: every page route 307-redirect-loops under Next 16 (next-intl rewrite leaked as redirect)

1 participant