Spring til indhold
← Tilbage til “Gaten er dig, del 2: produktionen stemte imod”
Artefaktprompt

The clean-room investigator brief

The full prompt handed to a fresh Claude session on 2026-07-03 at 11:56 — generated at the end of the muddied session, then run in a brand-new context with ultracode effort. Verbatim.

You are a clean-room investigator. Do NOT trust any prior analysis — I will give you a
hypothesis to confirm or refute with your own empirical evidence. If you cannot reproduce a
claim, say so. Every conclusion must be backed by a command output or a quoted line of source
code you actually read, not by reasoning alone.

## Background (the real-world symptom)

A Next.js app that uses next-intl for i18n routing is deployed as an output:'standalone'
container behind Azure Container Apps (ACA). The container listens on port 3000; ACA's ingress
terminates TLS on 443 and reverse-proxies to it. In production, GET / returns a 307 redirect
whose Location is:

    https://<public-host>:3000/en      ← the internal port :3000 leaks into the public URL

It should be https://<public-host>/en (no port). Requesting /en directly returns 200. The
public host is CORRECT; only the :3000 port is wrong.

## Hypothesis to test (confirm or refute each point independently)

1. next-intl builds the locale-redirect's PORT solely from the x-forwarded-port request header
   (not from Host, not from request.url).
2. In an output:'standalone' server, Next.js BACKFILLS x-forwarded-port with its own listen
   port (3000) using nullish-assignment (??=) — it only fills the header when the proxy did NOT
   send one. Proxy sends x-forwarded-port → Next respects it; proxy omits it → Next injects 3000.
3. ACA's ingress sends X-Forwarded-Proto but does NOT send X-Forwarded-Port (nor
   X-Forwarded-Host). That omission is what triggers the leak.
4. experimental.trustHostHeader does NOT fix this in current Next.js standalone — its code
   branch is effectively dead when the standalone server sets HOSTNAME+PORT. (This is the point
   I am most unsure about — investigate it hardest and tell me the truth.)
5. The correct fix is to normalize x-forwarded-port in middleware (proxy.ts) from the trusted
   x-forwarded-proto (https→443, http→80) — the Next equivalent of ASP.NET Core's
   UseForwardedHeaders.

## Your task

Build a MINIMAL, brand-new reproduction from scratch and produce a matrix of evidence.

### 1. Scaffold a minimal newest Next.js + next-intl app
- Use the latest stable next and next-intl (record exact versions from package.json after
  install). App Router, TypeScript.
- Standard documented next-intl setup: two locales (en, da), en default, locale-prefixed
  routing so GET / 307-redirects to /en. A proxy.ts (Next 16) / middleware.ts (Next ≤15) that
  calls the next-intl middleware. One trivial page.
- next.config.ts with output:'standalone'.
- GOTCHA: with Turbopack (default next build in Next 16) a symlinked node_modules fails with
  "Symlink points out of the filesystem root" — do a real npm install, don't symlink.

### 2. Reproduce the symptom against the built standalone server
Run the standalone server the way a container does and probe with curl:

    PORT=3099 HOSTNAME=0.0.0.0 NODE_ENV=production node .next/standalone/server.js &
    curl -sI <headers...> http://127.0.0.1:3099/ | grep -i '^location:'

Produce this table (fill in the actual observed Location for each):

    | # | Host header        | X-Forwarded-Proto | X-Forwarded-Port | observed Location |
    |---|--------------------|-------------------|------------------|-------------------|
    | A | (none, direct hit) | (none)            | (none)           | ?                 |
    | B | public.example.io  | https             | (none)  ← ACA    | ?                 |
    | C | public.example.io  | https             | 443              | ?                 |
    | D | public.example.io  | https             | 3000             | ?                 |

Case B is the ACA production shape. Confirm whether it reproduces the :port leak.

### 3. Prove WHERE the port comes from (read the source, quote the lines)
- Open the installed next-intl middleware source (node_modules/next-intl/dist/.../middleware/).
  Find the redirect construction and quote the exact lines that set the URL's port.
  Confirm/refute hypothesis #1.
- Open node_modules/next/dist/server/base-server.js (or wherever the standalone server injects
  forwarded headers). Quote the exact lines that set x-forwarded-port/host/proto.
  Confirm/refute hypothesis #2 (is it ??= respect-then-backfill, value this.port?).

### 4. Settle the trustHostHeader question empirically (the key ask)
- Add experimental:{ trustHostHeader:true } to next.config.ts (may be untyped — cast as
  needed). Rebuild.
- Verify it was baked: grep trustHostHeader in .next/standalone/.next/required-server-files.json
  and report its value. Note any "Unrecognized key" build warning.
- Re-run the Case B probe. Does the :port still leak, yes/no?
- Then find the runtime code that consumes trustHostHeader (search Next's dist for
  trustHostHeader, e.g. attachRequestMeta / origin resolution). Quote the surrounding branch and
  explain, FROM THE CODE, why it does or does not take effect when the standalone server has set
  HOSTNAME/PORT. This is the crux — source-backed answer, not a guess. If trustHostHeader DOES
  fix it, say so plainly and show the passing curl.

### 5. Test the proposed fix
- Add a middleware step that, before calling the next-intl middleware, clones the request
  headers and sets x-forwarded-port to 443 when x-forwarded-proto is https (80 for http), but
  ONLY when the forwarded/Host value carries no explicit port (so a direct localhost:3099 dev
  hit is left alone). Re-run all four cases; show B, C, D all yield clean
  https://public.example.io/en while A stays http://127.0.0.1:3099/en.
- Note whether recreating the request as new NextRequest(request,{headers}) preserves
  method/body (does a POST through middleware still work?).

### 6. Deliverables
- Exact next / next-intl versions used.
- The filled-in case matrix (before fix, with trustHostHeader, after fix).
- The quoted source lines for #3 and #4.
- A one-paragraph verdict on each of the 5 hypotheses: CONFIRMED / REFUTED / PARTIAL, with
  evidence.
- Keep the repro in a fresh temp directory; print paths, commit nothing.

Be skeptical. If any hypothesis is wrong, I specifically want to know — that's the point.