The demo wouldn't load. We had just deployed the marketplace product to Azure Container Apps — passwordless Entra auth, Premium SSD storage, the works — and the public URL answered with a redirect to https://host:3000/en. Port 3000. The internal port, leaking into a public redirect, on a managed platform that never exposes it.
Claude quickly produced a fix: strip the port from the redirect in our middleware. stripInternalPort. It probably would have worked, too. But something smelled off.
"I've never seen this before"
After twenty years behind a proxy or two, you know what this is supposed to look like: the reverse proxy sets forwarded headers, the framework reads them, done. You don't strip ports yourself. So I wrote back:
(The prompt is verbatim — typos and all. That's what they look like when you type them in the flow.)
Claude came back with an explanation that sounded entirely convincing: Azure Container Apps' ingress injects x-forwarded-port: 3000, and that's why next-intl builds the broken redirect. Plausible. Confident. And — it turned out — wrong.
Gate 1: "How did you get to that conclusion?"
This is where the most important habit in agentic development kicks in. Not rejecting the answer — demanding to know where it came from:
We ended up with something even cheaper than a dumb .NET logger: we temporarily swapped the marketplace container's image for traefik/whoami — same container app, same ingress, same FQDN, only the image changed — and read the raw headers exactly as the app receives them.
The result disproved Claude's diagnosis. ACA sends a clean Host header with the public hostname (no port), X-Forwarded-Proto: https, and X-Forwarded-For. No X-Forwarded-Port. No X-Forwarded-Host. Claude's earlier claim came from a curl-contaminated reading. The assumption was wrong — and now we had proof, not just a feeling.
Gate 2: "We've solved this before — the clean way"
The next pushback was architectural. In the .NET world, UseForwardedHeaders handles this as one of the very first steps in the pipeline: it rewrites the request URL so everything downstream can simply trust it. No package has to grub around in headers afterwards:
The research round dug up something interesting: our sibling app (booking, also next-intl) had exactly the same latent bug and had "solved" it with the same post-hoc hack. And the helper we could have reused, extract-x-forwarded-headers.ts, keeps the internal port when x-forwarded-host is absent — it would have broken in precisely the same way. The plausible fix was already becoming a pattern across the codebase.
Gate 3: "Is this even an x-forwarded problem?"
Claude built a tidy normalizeForwardedUrl middleware — the .NET style, rewrite the URL first. Better. But the question kept nagging: why should this be necessary at all?
And that's where the case cracked. Claude went into Next.js' own source code and found the line (resolve-routes.js in Next 16.2.4):
const initUrl = config.experimental.trustHostHeader
? `https://${req.headers.host || 'localhost'}${req.url}`
: opts.port
? `${protocol}://${formatHostname(opts.hostname || 'localhost')}:${opts.port}${req.url}`
: req.url || '';
It was never an ACA problem. It was never a next-intl problem. It was never an x-forwarded problem. By default, Next.js does not trust the Host header — it builds request.url from its own listen address, localhost:3000, ignoring the public hostname it is actually being handed. next-intl just faithfully copies that internal origin into its redirect. The AI had been chasing symptoms across three layers — the ingress, the i18n package, the headers — without seeing that the foundation under all of them was the problem.
The end result was a small, env-gated normalization in proxy.ts (because Next's own experimental.trustHostHeader turns out to be frozen at build time under standalone output, so it can't be driven by a container env var — another detail only the source code revealed). The same fix in both apps, the flawed old helper deleted, and the whole pattern codified into the nextjs-aspire-hosting skill so the next project gets the solution for free.
So who won?
Here's the honest part, and the reason this post isn't titled "the AI was wrong".
The first fix — strip the port — would have worked. The demo would have loaded, the customer would have moved on, and the port would most likely never have been a problem again. Three gates, one whoami deployment, source-code research in two packages, and a middleware rewrite later, we have... a demo that loads. Exactly as it would have anyway.
But we also have: one root cause instead of three myths. A sibling app repaired before its bug became a production incident. A deleted helper that was waiting to bite someone. And a skill that means none of our Next.js apps has to go through this afternoon again.
Was it worth the extra hours? That's the real question, and the answer isn't automatically yes. Had this been an internal prototype, I'd have taken the port strip and been home before dinner. What tipped it here was that the fix was becoming a pattern — copied into app after app as "how we do it". Technical debt is rarely one hack; it's a hack that has children.
So: the AI delivered both solutions, the quick one and the right one. I delivered the one thing it couldn't — the feeling of I've never seen this before, and I should have. The gate in an agentic workflow isn't a review step in a pipeline. The gate is you and your experience, and it only works if you actually pull the handle when something smells.
And who won? I got a better solution and the satisfaction of catching my partner in a mistake. Claude got to be right in the end — it was, after all, the one that found the line in Next.js' source. But the only party that won on both solutions, including the wrong diagnosis along the way, was Anthropic. They sell tokens for the entire detective story — including the chapters where the detective is wrong.
Maybe that's the neatest summary of agentic development in 2026: mistakes have become cheap enough to be part of the product. You just pay for them in the same currency as the successes.
Update, July 3: It turned out we were still wrong. The fix didn't work in production, and the "real" root cause above was myth number three. The story continues in part 2: production voted no.
