Im upfront: The description and reproduction were created with (the help of) claude. The bug is real within our application.
Astro Info
Astro v6.4.2
Node v22.x
System Linux (Docker, behind TLS-terminating edge proxy)
Package Manager pnpm
Output server
Adapter @astrojs/node 10.1.2 (mode: 'middleware')
Integrations @astrojs/react
`security.allowedDomains` is configured (so forwarded-header validation is opted into):
// astro.config.mjs
export default defineConfig({
output: 'server',
security: {
allowedDomains: [{ protocol: 'https', hostname: '**.example.com' }],
},
adapter: node({ mode: 'middleware' }),
});
If this issue only occurs in one browser, which browser is a problem?
No response
Describe the Bug
Since @astrojs/node 10.1 (we upgraded 10.0.5 → 10.1.2 together with astro 6.1 → 6.4), the URL of the Request object exposed to middleware and pages (context.request.url / Astro.request.url) no longer honors X-Forwarded-Proto / X-Forwarded-Host, even when the forwarded host is validated against security.allowedDomains.
Behind a TLS-terminating proxy (edge does HTTPS, internal hop to the Node server is plain HTTP), request.url now always reports http:// and the internal Host, while Astro.url / context.url correctly report the forwarded origin. The two now permanently disagree:
// middleware behind a proxy sending X-Forwarded-Proto: https
export const onRequest = defineMiddleware(({ url, request }, next) => {
console.log(url.href); // https://app.example.com/page ✅ forwarded headers applied
console.log(request.url); // http://app.example.com/page ❌ socket-derived only
return next();
});
In 10.0.5 both reported https://app.example.com/page.
Root cause
The adapter previously built the Request via createRequest from astro/app/node, which resolves X-Forwarded-Proto/Host/Port (validated against allowedDomains via validateForwardedHeaders) into the Request URL itself.
#16811 (commit e0e26db, "Resolve X-Forwarded-* headers inside FetchState") moved forwarded-header resolution into FetchState's constructor and, in the same change, switched the adapter's request construction in packages/integrations/node/src/serve-app.ts:
-import { createRequest, writeResponse, getAbortControllerCleanup } from 'astro/app/node';
+import { createRequestFromNodeRequest, writeResponse, getAbortControllerCleanup } from 'astro/app/node';
...
- request = createRequest(req, {
+ request = createRequestFromNodeRequest(req, {
createRequestFromNodeRequest derives the protocol exclusively from req.socket.encrypted and the host from the Host header (packages/astro/src/core/app/node.ts). The allowedDomains option it receives is only used to decide whether to trust x-forwarded-for for clientAddress — never for URL construction.
The new resolution site, FetchState#applyForwardedHeaders() (packages/astro/src/core/fetch/fetch-state.ts), updates only this.url and this.clientAddress. It does not reconstruct this.request, so the Request keeps the socket-derived URL for its entire lifetime:
// fetch-state.js — updates this.url only; this.request is untouched
#applyForwardedHeaders() {
...
if (validated.protocol) this.url.protocol = validated.protocol + ':';
if (validated.host) { this.url.hostname = ...; }
...
}
So #16811 fixed Astro.url for the FetchState path but silently dropped the forwarded origin from Astro.request.url, which createRequest had been providing.
Impact
Any server code that derives absolute URLs from Astro.request.url (or new URL(request.url)) breaks behind a proxy:
- auth/session libraries that take the
Request object and read its URL (callback URLs, redirect targets),
- code generating absolute links — in our case a navigation service whose server-rendered link
hrefs flipped from https:// to http://, breaking client-side URL matching,
- anything comparing
request.url against context.url (they now differ in origin).
The regression is invisible in local dev (no proxy) and only manifests in proxied deployments.
What's the expected result?
When the forwarded host validates against security.allowedDomains (the explicit opt-in), Astro.request.url should reflect the forwarded origin — matching both Astro.url and the 10.0.5 behavior of createRequest.
Possible fixes:
- Have the node adapter resolve validated forwarded headers when constructing the Request again — e.g. make
createRequestFromNodeRequest apply validateForwardedHeaders to the URL (it already receives allowedDomains), or switch serve-app.ts back to createRequest.
- Alternatively, have
FetchState#applyForwardedHeaders() reconstruct this.request with the resolved URL so request.url and this.url can't disagree.
Either way the validation gate on security.allowedDomains is preserved, so this doesn't reopen the host-header-injection surface that the recent hardening addressed — without allowedDomains configured, forwarded headers would remain untrusted exactly as today.
Link to Minimal Reproducible Example
https://github.com/beckerei/astro-forwarded-url-repro
Participation
Im upfront: The description and reproduction were created with (the help of) claude. The bug is real within our application.
Astro Info
If this issue only occurs in one browser, which browser is a problem?
No response
Describe the Bug
Since
@astrojs/node10.1 (we upgraded 10.0.5 → 10.1.2 together with astro 6.1 → 6.4), the URL of theRequestobject exposed to middleware and pages (context.request.url/Astro.request.url) no longer honorsX-Forwarded-Proto/X-Forwarded-Host, even when the forwarded host is validated againstsecurity.allowedDomains.Behind a TLS-terminating proxy (edge does HTTPS, internal hop to the Node server is plain HTTP),
request.urlnow always reportshttp://and the internalHost, whileAstro.url/context.urlcorrectly report the forwarded origin. The two now permanently disagree:In 10.0.5 both reported
https://app.example.com/page.Root cause
The adapter previously built the Request via
createRequestfromastro/app/node, which resolvesX-Forwarded-Proto/Host/Port(validated againstallowedDomainsviavalidateForwardedHeaders) into the Request URL itself.#16811 (commit e0e26db, "Resolve X-Forwarded-* headers inside FetchState") moved forwarded-header resolution into
FetchState's constructor and, in the same change, switched the adapter's request construction inpackages/integrations/node/src/serve-app.ts:createRequestFromNodeRequestderives the protocol exclusively fromreq.socket.encryptedand the host from theHostheader (packages/astro/src/core/app/node.ts). TheallowedDomainsoption it receives is only used to decide whether to trustx-forwarded-forforclientAddress— never for URL construction.The new resolution site,
FetchState#applyForwardedHeaders()(packages/astro/src/core/fetch/fetch-state.ts), updates onlythis.urlandthis.clientAddress. It does not reconstructthis.request, so the Request keeps the socket-derived URL for its entire lifetime:So #16811 fixed
Astro.urlfor theFetchStatepath but silently dropped the forwarded origin fromAstro.request.url, whichcreateRequesthad been providing.Impact
Any server code that derives absolute URLs from
Astro.request.url(ornew URL(request.url)) breaks behind a proxy:Requestobject and read its URL (callback URLs, redirect targets),hrefs flipped fromhttps://tohttp://, breaking client-side URL matching,request.urlagainstcontext.url(they now differ in origin).The regression is invisible in local dev (no proxy) and only manifests in proxied deployments.
What's the expected result?
When the forwarded host validates against
security.allowedDomains(the explicit opt-in),Astro.request.urlshould reflect the forwarded origin — matching bothAstro.urland the 10.0.5 behavior ofcreateRequest.Possible fixes:
createRequestFromNodeRequestapplyvalidateForwardedHeadersto the URL (it already receivesallowedDomains), or switchserve-app.tsback tocreateRequest.FetchState#applyForwardedHeaders()reconstructthis.requestwith the resolved URL sorequest.urlandthis.urlcan't disagree.Either way the validation gate on
security.allowedDomainsis preserved, so this doesn't reopen the host-header-injection surface that the recent hardening addressed — withoutallowedDomainsconfigured, forwarded headers would remain untrusted exactly as today.Link to Minimal Reproducible Example
https://github.com/beckerei/astro-forwarded-url-repro
Participation