diff --git a/.changeset/brave-pumpkins-train.md b/.changeset/brave-pumpkins-train.md
new file mode 100644
index 0000000000..61a7492406
--- /dev/null
+++ b/.changeset/brave-pumpkins-train.md
@@ -0,0 +1,5 @@
+---
+"astro": patch
+---
+
+Fixes an issue where forwarded requests did not include hostname on node-based adapters. This also makes error pages more reliable.
diff --git a/packages/astro/src/core/app/node.ts b/packages/astro/src/core/app/node.ts
index 0795c8dd6f..df2fa12d9a 100644
--- a/packages/astro/src/core/app/node.ts
+++ b/packages/astro/src/core/app/node.ts
@@ -63,8 +63,9 @@ export class NodeApp extends App {
const protocol =
req.headers['x-forwarded-proto'] ??
('encrypted' in req.socket && req.socket.encrypted ? 'https' : 'http');
- const hostname = req.headers.host || req.headers[':authority'];
- const url = `${protocol}://${hostname}${req.url}`;
+ const hostname = req.headers["x-forwarded-host"] ?? req.headers.host ?? req.headers[":authority"];
+ const port = req.headers["x-forwarded-port"];
+ const url = `${protocol}://${hostname}${port ? `:${port}` : ''}${req.url}`;
const options: RequestInit = {
method: req.method || 'GET',
headers: makeRequestHeaders(req),
diff --git a/packages/integrations/node/test/fixtures/url-protocol/src/pages/index.astro b/packages/integrations/node/test/fixtures/url-protocol/src/pages/index.astro
deleted file mode 100644
index 61fb9867b1..0000000000
--- a/packages/integrations/node/test/fixtures/url-protocol/src/pages/index.astro
+++ /dev/null
@@ -1,11 +0,0 @@
----
----
-
-
-
- url-protocol
-
-
- {Astro.url.protocol}
-
-
diff --git a/packages/integrations/node/test/fixtures/url-protocol/package.json b/packages/integrations/node/test/fixtures/url/package.json
similarity index 80%
rename from packages/integrations/node/test/fixtures/url-protocol/package.json
rename to packages/integrations/node/test/fixtures/url/package.json
index 4b0775716a..f349011fd3 100644
--- a/packages/integrations/node/test/fixtures/url-protocol/package.json
+++ b/packages/integrations/node/test/fixtures/url/package.json
@@ -1,5 +1,5 @@
{
- "name": "@test/url-protocol",
+ "name": "@test/url",
"version": "0.0.0",
"private": true,
"dependencies": {
diff --git a/packages/integrations/node/test/fixtures/url/src/pages/index.astro b/packages/integrations/node/test/fixtures/url/src/pages/index.astro
new file mode 100644
index 0000000000..003429f520
--- /dev/null
+++ b/packages/integrations/node/test/fixtures/url/src/pages/index.astro
@@ -0,0 +1,9 @@
+---
+---
+
+
+
+ URL
+
+ {Astro.url.href}
+
diff --git a/packages/integrations/node/test/url-protocol.test.js b/packages/integrations/node/test/url.test.js
similarity index 62%
rename from packages/integrations/node/test/url-protocol.test.js
rename to packages/integrations/node/test/url.test.js
index 94d53104b9..39d4665272 100644
--- a/packages/integrations/node/test/url-protocol.test.js
+++ b/packages/integrations/node/test/url.test.js
@@ -3,14 +3,15 @@ import { before, describe, it } from 'node:test';
import { TLSSocket } from 'node:tls';
import nodejs from '../dist/index.js';
import { createRequestAndResponse, loadFixture } from './test-utils.js';
+import * as cheerio from 'cheerio';
-describe('URL protocol', () => {
- /** @type {import('./test-utils').Fixture} */
+describe('URL', () => {
+ /** @type {import('./test-utils.js').Fixture} */
let fixture;
before(async () => {
fixture = await loadFixture({
- root: './fixtures/url-protocol/',
+ root: './fixtures/url/',
output: 'server',
adapter: nodejs({ mode: 'standalone' }),
});
@@ -18,7 +19,7 @@ describe('URL protocol', () => {
});
it('return http when non-secure', async () => {
- const { handler } = await import('./fixtures/url-protocol/dist/server/entry.mjs');
+ const { handler } = await import('./fixtures/url/dist/server/entry.mjs');
let { req, res, text } = createRequestAndResponse({
url: '/',
});
@@ -31,7 +32,7 @@ describe('URL protocol', () => {
});
it('return https when secure', async () => {
- const { handler } = await import('./fixtures/url-protocol/dist/server/entry.mjs');
+ const { handler } = await import('./fixtures/url/dist/server/entry.mjs');
let { req, res, text } = createRequestAndResponse({
socket: new TLSSocket(),
url: '/',
@@ -45,7 +46,7 @@ describe('URL protocol', () => {
});
it('return http when the X-Forwarded-Proto header is set to http', async () => {
- const { handler } = await import('./fixtures/url-protocol/dist/server/entry.mjs');
+ const { handler } = await import('./fixtures/url/dist/server/entry.mjs');
let { req, res, text } = createRequestAndResponse({
headers: { 'X-Forwarded-Proto': 'http' },
url: '/',
@@ -59,7 +60,7 @@ describe('URL protocol', () => {
});
it('return https when the X-Forwarded-Proto header is set to https', async () => {
- const { handler } = await import('./fixtures/url-protocol/dist/server/entry.mjs');
+ const { handler } = await import('./fixtures/url/dist/server/entry.mjs');
let { req, res, text } = createRequestAndResponse({
headers: { 'X-Forwarded-Proto': 'https' },
url: '/',
@@ -71,4 +72,24 @@ describe('URL protocol', () => {
const html = await text();
assert.equal(html.includes('https:'), true);
});
+
+ it('includes forwarded host and port in the url', async () => {
+ const { handler } = await import('./fixtures/url/dist/server/entry.mjs');
+ let { req, res, text } = createRequestAndResponse({
+ headers: {
+ 'X-Forwarded-Proto': 'https',
+ 'X-Forwarded-Host': 'abc.xyz',
+ 'X-Forwarded-Port': '444'
+ },
+ url: '/',
+ });
+
+ handler(req, res);
+ req.send();
+
+ const html = await text();
+ const $ = cheerio.load(html);
+
+ assert.equal($('body').text(), "https://abc.xyz:444/");
+ });
});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b0db54548a..51abebd49d 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -4703,7 +4703,7 @@ importers:
specifier: workspace:*
version: link:../../../../../astro
- packages/integrations/node/test/fixtures/url-protocol:
+ packages/integrations/node/test/fixtures/url:
dependencies:
'@astrojs/node':
specifier: workspace:*