diff --git a/.changeset/odd-rats-drop.md b/.changeset/odd-rats-drop.md new file mode 100644 index 000000000000..1b2f930f0096 --- /dev/null +++ b/.changeset/odd-rats-drop.md @@ -0,0 +1,6 @@ +--- +'astro': patch +'@astrojs/node': patch +--- + +Fix `Astro.url.protocol` when using the @astrojs/node SSR adapter with HTTPS diff --git a/packages/astro/src/core/app/node.ts b/packages/astro/src/core/app/node.ts index 801ede4c9e06..2ab662d6698a 100644 --- a/packages/astro/src/core/app/node.ts +++ b/packages/astro/src/core/app/node.ts @@ -3,13 +3,18 @@ import type { SerializedSSRManifest, SSRManifest } from './types'; import * as fs from 'fs'; import { IncomingMessage } from 'http'; +import { TLSSocket } from 'tls'; import { deserializeManifest } from './common.js'; import { App, MatchOptions } from './index.js'; const clientAddressSymbol = Symbol.for('astro.clientAddress'); function createRequestFromNodeRequest(req: IncomingMessage, body?: Uint8Array): Request { - let url = `http://${req.headers.host}${req.url}`; + const protocol = + req.socket instanceof TLSSocket || req.headers['x-forwarded-proto'] === 'https' + ? 'https' + : 'http'; + let url = `${protocol}://${req.headers.host}${req.url}`; let rawHeaders = req.headers as Record; const entries = Object.entries(rawHeaders); const method = req.method || 'GET'; diff --git a/packages/integrations/node/src/standalone.ts b/packages/integrations/node/src/standalone.ts index d68c3a50032b..789269860308 100644 --- a/packages/integrations/node/src/standalone.ts +++ b/packages/integrations/node/src/standalone.ts @@ -1,4 +1,5 @@ import type { NodeApp } from 'astro/app/node'; +import https from 'https'; import path from 'path'; import { fileURLToPath } from 'url'; import { createServer } from './http-server.js'; @@ -53,8 +54,9 @@ export default function startServer(app: NodeApp, options: Options) { handler ); + const protocol = server.server instanceof https.Server ? 'https' : 'http'; // eslint-disable-next-line no-console - console.log(`Server listening on http://${host}:${port}`); + console.log(`Server listening on ${protocol}://${host}:${port}`); return server.closed(); } diff --git a/packages/integrations/node/test/fixtures/url-protocol/package.json b/packages/integrations/node/test/fixtures/url-protocol/package.json new file mode 100644 index 000000000000..4b0775716a3e --- /dev/null +++ b/packages/integrations/node/test/fixtures/url-protocol/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/url-protocol", + "version": "0.0.0", + "private": true, + "dependencies": { + "astro": "workspace:*", + "@astrojs/node": "workspace:*" + } +} 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 new file mode 100644 index 000000000000..61fb9867b15f --- /dev/null +++ b/packages/integrations/node/test/fixtures/url-protocol/src/pages/index.astro @@ -0,0 +1,11 @@ +--- +--- + + + + url-protocol + + + {Astro.url.protocol} + + diff --git a/packages/integrations/node/test/url-protocol.test.js b/packages/integrations/node/test/url-protocol.test.js new file mode 100644 index 000000000000..0da4bdeb03b0 --- /dev/null +++ b/packages/integrations/node/test/url-protocol.test.js @@ -0,0 +1,73 @@ +import { TLSSocket } from 'tls'; +import nodejs from '../dist/index.js'; +import { loadFixture, createRequestAndResponse } from './test-utils.js'; +import { expect } from 'chai'; + +describe('URL protocol', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/url-protocol/', + output: 'server', + adapter: nodejs({ mode: 'standalone' }), + }); + await fixture.build(); + }); + + it('return http when non-secure', async () => { + const { handler } = await import('./fixtures/url-protocol/dist/server/entry.mjs'); + let { req, res, text } = createRequestAndResponse({ + url: '/', + }); + + handler(req, res); + req.send(); + + const html = await text(); + expect(html).to.include('http:'); + }); + + it('return https when secure', async () => { + const { handler } = await import('./fixtures/url-protocol/dist/server/entry.mjs'); + let { req, res, text } = createRequestAndResponse({ + socket: new TLSSocket(), + url: '/', + }); + + handler(req, res); + req.send(); + + const html = await text(); + expect(html).to.include('https:'); + }); + + 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'); + let { req, res, text } = createRequestAndResponse({ + headers: { 'X-Forwarded-Proto': 'http' }, + url: '/', + }); + + handler(req, res); + req.send(); + + const html = await text(); + expect(html).to.include('http:'); + }); + + 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'); + let { req, res, text } = createRequestAndResponse({ + headers: { 'X-Forwarded-Proto': 'https' }, + url: '/', + }); + + handler(req, res); + req.send(); + + const html = await text(); + expect(html).to.include('https:'); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5dc3bf7eb972..454931ff2f55 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3109,6 +3109,14 @@ importers: '@astrojs/node': link:../../.. astro: link:../../../../../astro + packages/integrations/node/test/fixtures/url-protocol: + specifiers: + '@astrojs/node': workspace:* + astro: workspace:* + dependencies: + '@astrojs/node': link:../../.. + astro: link:../../../../../astro + packages/integrations/node/test/fixtures/well-known-locations: specifiers: '@astrojs/node': workspace:*