From 5a23483efb3ba614b05a00064f84415620605204 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Tue, 19 Jul 2022 16:10:15 -0400 Subject: [PATCH] Adds support for Astro.clientAddress (#3973) * Adds support for Astro.clientAddress * Pass through mode and adapterName in SSG * Pass through the mode provided * Provide an adapter specific error message when possible --- .changeset/olive-dryers-sell.md | 18 +++ packages/astro/src/@types/astro.ts | 4 + packages/astro/src/core/app/index.ts | 65 +++++---- packages/astro/src/core/app/node.ts | 8 +- packages/astro/src/core/app/types.ts | 1 + packages/astro/src/core/build/generate.ts | 2 + packages/astro/src/core/build/index.ts | 36 +++-- packages/astro/src/core/build/static-build.ts | 2 +- packages/astro/src/core/build/types.ts | 2 + .../astro/src/core/build/vite-plugin-ssr.ts | 2 +- packages/astro/src/core/render/core.ts | 7 + packages/astro/src/core/render/dev/index.ts | 2 + packages/astro/src/core/render/result.ts | 16 +++ packages/astro/src/core/request.ts | 6 + .../src/vite-plugin-astro-server/index.ts | 1 + packages/astro/test/client-address.test.js | 130 ++++++++++++++++++ .../client-address/src/pages/index.astro | 12 ++ packages/astro/test/test-adapter.js | 20 ++- packages/astro/test/test-utils.js | 3 +- .../integrations/cloudflare/src/server.ts | 1 + packages/integrations/deno/src/server.ts | 4 +- .../netlify/src/netlify-edge-functions.ts | 4 + .../netlify/src/netlify-functions.ts | 5 + .../vercel/src/edge/entrypoint.ts | 3 + .../src/serverless/request-transform.ts | 6 +- 25 files changed, 311 insertions(+), 49 deletions(-) create mode 100644 .changeset/olive-dryers-sell.md create mode 100644 packages/astro/test/client-address.test.js create mode 100644 packages/astro/test/fixtures/client-address/src/pages/index.astro diff --git a/.changeset/olive-dryers-sell.md b/.changeset/olive-dryers-sell.md new file mode 100644 index 000000000..15f3b531e --- /dev/null +++ b/.changeset/olive-dryers-sell.md @@ -0,0 +1,18 @@ +--- +'astro': minor +'@astrojs/cloudflare': minor +'@astrojs/deno': minor +'@astrojs/netlify': minor +'@astrojs/vercel': minor +'@astrojs/node': minor +--- + +Adds support for Astro.clientAddress + +The new `Astro.clientAddress` property allows you to get the IP address of the requested user. + +```astro +
Your address { Astro.clientAddress }
+``` + +This property is only available when building for SSR, and only if the adapter you are using supports providing the IP address. If you attempt to access the property in a SSG app it will throw an error. diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 766ee0607..3e1bfd4a8 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -92,6 +92,10 @@ export interface AstroGlobal extends AstroGlobalPartial { * [Astro reference](https://docs.astro.build/en/reference/api-reference/#astrocanonicalurl) */ canonicalURL: URL; + /** The address (usually IP address) of the user. Used with SSR only. + * + */ + clientAddress: string; /** Parameters passed to a dynamic page generated using [getStaticPaths](https://docs.astro.build/en/reference/api-reference/#getstaticpaths) * * Example usage: diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts index e407adea2..c3cc4d705 100644 --- a/packages/astro/src/core/app/index.ts +++ b/packages/astro/src/core/app/index.ts @@ -10,6 +10,7 @@ import type { RouteInfo, SSRManifest as Manifest } from './types'; import mime from 'mime'; import { call as callEndpoint } from '../endpoint/index.js'; +import { error } from '../logger/core.js'; import { consoleLogDestination } from '../logger/console.js'; import { joinPaths, prependForwardSlash } from '../path.js'; import { render } from '../render/core.js'; @@ -96,33 +97,43 @@ export class App { } } - const response = await render({ - links, - logging: this.#logging, - markdown: manifest.markdown, - mod, - origin: url.origin, - pathname: url.pathname, - scripts, - renderers, - async resolve(specifier: string) { - if (!(specifier in manifest.entryModules)) { - throw new Error(`Unable to resolve [${specifier}]`); - } - const bundlePath = manifest.entryModules[specifier]; - return bundlePath.startsWith('data:') - ? bundlePath - : prependForwardSlash(joinPaths(manifest.base, bundlePath)); - }, - route: routeData, - routeCache: this.#routeCache, - site: this.#manifest.site, - ssr: true, - request, - streaming: this.#streaming, - }); - - return response; + try { + const response = await render({ + adapterName: manifest.adapterName, + links, + logging: this.#logging, + markdown: manifest.markdown, + mod, + mode: 'production', + origin: url.origin, + pathname: url.pathname, + scripts, + renderers, + async resolve(specifier: string) { + if (!(specifier in manifest.entryModules)) { + throw new Error(`Unable to resolve [${specifier}]`); + } + const bundlePath = manifest.entryModules[specifier]; + return bundlePath.startsWith('data:') + ? bundlePath + : prependForwardSlash(joinPaths(manifest.base, bundlePath)); + }, + route: routeData, + routeCache: this.#routeCache, + site: this.#manifest.site, + ssr: true, + request, + streaming: this.#streaming, + }); + + return response; + } catch(err) { + error(this.#logging, 'ssr', err); + return new Response(null, { + status: 500, + statusText: 'Internal server error' + }); + } } async #callEndpoint( diff --git a/packages/astro/src/core/app/node.ts b/packages/astro/src/core/app/node.ts index 075c55c65..350deb211 100644 --- a/packages/astro/src/core/app/node.ts +++ b/packages/astro/src/core/app/node.ts @@ -5,13 +5,19 @@ import { IncomingMessage } from 'http'; import { deserializeManifest } from './common.js'; import { App } from './index.js'; +const clientAddressSymbol = Symbol.for('astro.clientAddress'); + function createRequestFromNodeRequest(req: IncomingMessage): Request { let url = `http://${req.headers.host}${req.url}`; - const entries = Object.entries(req.headers as Record); + let rawHeaders = req.headers as Record; + const entries = Object.entries(rawHeaders); let request = new Request(url, { method: req.method || 'GET', headers: new Headers(entries), }); + if(req.socket.remoteAddress) { + Reflect.set(request, clientAddressSymbol, req.socket.remoteAddress); + } return request; } diff --git a/packages/astro/src/core/app/types.ts b/packages/astro/src/core/app/types.ts index 09f0a79d2..587c924b9 100644 --- a/packages/astro/src/core/app/types.ts +++ b/packages/astro/src/core/app/types.ts @@ -25,6 +25,7 @@ export type SerializedRouteInfo = Omit & { }; export interface SSRManifest { + adapterName: string; routes: RouteInfo[]; site?: string; base?: string; diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index 1f88f6ce6..14980dbdb 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -210,10 +210,12 @@ async function generatePath( const ssr = isBuildingToSSR(opts.astroConfig); const url = new URL(opts.astroConfig.base + removeLeadingForwardSlash(pathname), origin); const options: RenderOptions = { + adapterName: undefined, links, logging, markdown: astroConfig.markdown, mod, + mode: opts.mode, origin, pathname, scripts, diff --git a/packages/astro/src/core/build/index.ts b/packages/astro/src/core/build/index.ts index ce8f485d4..90d42dd7c 100644 --- a/packages/astro/src/core/build/index.ts +++ b/packages/astro/src/core/build/index.ts @@ -1,5 +1,5 @@ import type { AstroTelemetry } from '@astrojs/telemetry'; -import type { AstroConfig, BuildConfig, ManifestData } from '../../@types/astro'; +import type { AstroConfig, BuildConfig, ManifestData, RuntimeMode } from '../../@types/astro'; import type { LogOptions } from '../logger/core'; import fs from 'fs'; @@ -24,7 +24,7 @@ import { staticBuild } from './static-build.js'; import { getTimeStat } from './util.js'; export interface BuildOptions { - mode?: string; + mode?: RuntimeMode; logging: LogOptions; telemetry: AstroTelemetry; } @@ -39,7 +39,7 @@ export default async function build(config: AstroConfig, options: BuildOptions): class AstroBuilder { private config: AstroConfig; private logging: LogOptions; - private mode = 'production'; + private mode: RuntimeMode = 'production'; private origin: string; private routeCache: RouteCache; private manifest: ManifestData; @@ -129,17 +129,25 @@ class AstroBuilder { colors.dim(`Completed in ${getTimeStat(this.timer.init, performance.now())}.`) ); - await staticBuild({ - allPages, - astroConfig: this.config, - logging: this.logging, - manifest: this.manifest, - origin: this.origin, - pageNames, - routeCache: this.routeCache, - viteConfig, - buildConfig, - }); + try { + await staticBuild({ + allPages, + astroConfig: this.config, + logging: this.logging, + manifest: this.manifest, + mode: this.mode, + origin: this.origin, + pageNames, + routeCache: this.routeCache, + viteConfig, + buildConfig, + }); + } catch(err: unknown) { + // If the build doesn't complete, still shutdown the Vite server so the process doesn't hang. + await viteServer.close(); + throw err; + } + // Write any additionally generated assets to disk. this.timer.assetsStart = performance.now(); diff --git a/packages/astro/src/core/build/static-build.ts b/packages/astro/src/core/build/static-build.ts index 47670d038..ce183b9cc 100644 --- a/packages/astro/src/core/build/static-build.ts +++ b/packages/astro/src/core/build/static-build.ts @@ -145,7 +145,7 @@ async function ssrBuild(opts: StaticBuildOptions, internals: BuildInternals, inp ...(viteConfig.plugins || []), // SSR needs to be last isBuildingToSSR(opts.astroConfig) && - vitePluginSSR(opts, internals, opts.astroConfig._ctx.adapter!), + vitePluginSSR(internals, opts.astroConfig._ctx.adapter!), vitePluginAnalyzer(opts.astroConfig, internals), ], publicDir: ssr ? false : viteConfig.publicDir, diff --git a/packages/astro/src/core/build/types.ts b/packages/astro/src/core/build/types.ts index 4b734fb9e..c460e8c20 100644 --- a/packages/astro/src/core/build/types.ts +++ b/packages/astro/src/core/build/types.ts @@ -4,6 +4,7 @@ import type { ComponentInstance, ManifestData, RouteData, + RuntimeMode, SSRLoadedRenderer, } from '../../@types/astro'; import type { ViteConfigWithSSR } from '../create-vite'; @@ -30,6 +31,7 @@ export interface StaticBuildOptions { buildConfig: BuildConfig; logging: LogOptions; manifest: ManifestData; + mode: RuntimeMode; origin: string; pageNames: string[]; routeCache: RouteCache; diff --git a/packages/astro/src/core/build/vite-plugin-ssr.ts b/packages/astro/src/core/build/vite-plugin-ssr.ts index 05ff80916..feedadea2 100644 --- a/packages/astro/src/core/build/vite-plugin-ssr.ts +++ b/packages/astro/src/core/build/vite-plugin-ssr.ts @@ -20,7 +20,6 @@ const manifestReplace = '@@ASTRO_MANIFEST_REPLACE@@'; const replaceExp = new RegExp(`['"](${manifestReplace})['"]`, 'g'); export function vitePluginSSR( - buildOpts: StaticBuildOptions, internals: BuildInternals, adapter: AstroAdapter ): VitePlugin { @@ -153,6 +152,7 @@ function buildManifest( 'data:text/javascript;charset=utf-8,//[no before-hydration script]'; const ssrManifest: SerializedSSRManifest = { + adapterName: opts.astroConfig._ctx.adapter!.name, routes, site: astroConfig.site, base: astroConfig.base, diff --git a/packages/astro/src/core/render/core.ts b/packages/astro/src/core/render/core.ts index 1abc89363..df8cb49d2 100644 --- a/packages/astro/src/core/render/core.ts +++ b/packages/astro/src/core/render/core.ts @@ -4,6 +4,7 @@ import type { Params, Props, RouteData, + RuntimeMode, SSRElement, SSRLoadedRenderer, } from '../../@types/astro'; @@ -66,11 +67,13 @@ export async function getParamsAndProps( } export interface RenderOptions { + adapterName: string | undefined; logging: LogOptions; links: Set; styles?: Set; markdown: MarkdownRenderingOptions; mod: ComponentInstance; + mode: RuntimeMode; origin: string; pathname: string; scripts: Set; @@ -86,12 +89,14 @@ export interface RenderOptions { export async function render(opts: RenderOptions): Promise { const { + adapterName, links, styles, logging, origin, markdown, mod, + mode, pathname, scripts, renderers, @@ -126,10 +131,12 @@ export async function render(opts: RenderOptions): Promise { throw new Error(`Expected an exported Astro component but received typeof ${typeof Component}`); const result = createResult({ + adapterName, links, styles, logging, markdown, + mode, origin, params, props: pageProps, diff --git a/packages/astro/src/core/render/dev/index.ts b/packages/astro/src/core/render/dev/index.ts index bc048bdce..e3b6f0ac7 100644 --- a/packages/astro/src/core/render/dev/index.ts +++ b/packages/astro/src/core/render/dev/index.ts @@ -161,11 +161,13 @@ export async function render( }); let response = await coreRender({ + adapterName: astroConfig.adapter?.name, links, styles, logging, markdown: astroConfig.markdown, mod, + mode, origin, pathname, scripts, diff --git a/packages/astro/src/core/render/result.ts b/packages/astro/src/core/render/result.ts index e754b334a..4b341a575 100644 --- a/packages/astro/src/core/render/result.ts +++ b/packages/astro/src/core/render/result.ts @@ -6,6 +6,7 @@ import type { Page, Params, Props, + RuntimeMode, SSRElement, SSRLoadedRenderer, SSRResult, @@ -15,6 +16,8 @@ import { LogOptions, warn } from '../logger/core.js'; import { isScriptRequest } from './script.js'; import { createCanonicalURL, isCSSRequest } from './util.js'; +const clientAddressSymbol = Symbol.for('astro.clientAddress'); + function onlyAvailableInSSR(name: string) { return function _onlyAvailableInSSR() { // TODO add more guidance when we have docs and adapters. @@ -23,11 +26,13 @@ function onlyAvailableInSSR(name: string) { } export interface CreateResultArgs { + adapterName: string | undefined; ssr: boolean; streaming: boolean; logging: LogOptions; origin: string; markdown: MarkdownRenderingOptions; + mode: RuntimeMode; params: Params; pathname: string; props: Props; @@ -151,6 +156,17 @@ export function createResult(args: CreateResultArgs): SSRResult { const Astro = { __proto__: astroGlobal, canonicalURL, + get clientAddress() { + if(!(clientAddressSymbol in request)) { + if(args.adapterName) { + throw new Error(`Astro.clientAddress is not available in the ${args.adapterName} adapter. File an issue with the adapter to add support.`); + } else { + throw new Error(`Astro.clientAddress is not available in your environment. Ensure that you are using an SSR adapter that supports this feature.`) + } + } + + return Reflect.get(request, clientAddressSymbol); + }, params, props, request, diff --git a/packages/astro/src/core/request.ts b/packages/astro/src/core/request.ts index 9808a4e33..87b95a7a8 100644 --- a/packages/astro/src/core/request.ts +++ b/packages/astro/src/core/request.ts @@ -7,6 +7,7 @@ type RequestBody = ArrayBuffer | Blob | ReadableStream | URLSearchParams | FormD export interface CreateRequestOptions { url: URL | string; + clientAddress?: string | undefined; headers: HeaderType; method?: string; body?: RequestBody | undefined; @@ -14,9 +15,12 @@ export interface CreateRequestOptions { ssr: boolean; } +const clientAddressSymbol = Symbol.for('astro.clientAddress'); + export function createRequest({ url, headers, + clientAddress, method = 'GET', body = undefined, logging, @@ -67,6 +71,8 @@ export function createRequest({ return _headers; }, }); + } else if(clientAddress) { + Reflect.set(request, clientAddressSymbol, clientAddress); } return request; diff --git a/packages/astro/src/vite-plugin-astro-server/index.ts b/packages/astro/src/vite-plugin-astro-server/index.ts index 0312e4a3c..0b8cfd9b7 100644 --- a/packages/astro/src/vite-plugin-astro-server/index.ts +++ b/packages/astro/src/vite-plugin-astro-server/index.ts @@ -229,6 +229,7 @@ async function handleRequest( body, logging, ssr: buildingToSSR, + clientAddress: buildingToSSR ? req.socket.remoteAddress : undefined, }); try { diff --git a/packages/astro/test/client-address.test.js b/packages/astro/test/client-address.test.js new file mode 100644 index 000000000..8be85cc24 --- /dev/null +++ b/packages/astro/test/client-address.test.js @@ -0,0 +1,130 @@ +import { expect } from 'chai'; +import { loadFixture } from './test-utils.js'; +import testAdapter from './test-adapter.js'; +import { nodeLogDestination } from '../dist/core/logger/node.js'; +import * as cheerio from 'cheerio'; + +describe('Astro.clientAddress', () => { + describe('SSR', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/client-address/', + experimental: { + ssr: true, + }, + adapter: testAdapter(), + }); + }); + + describe('Production', () => { + before(async () => { + await fixture.build(); + }); + + it('Can get the address', async () => { + const app = await fixture.loadTestAdapterApp(); + const request = new Request('http://example.com/'); + const response = await app.render(request); + const html = await response.text(); + const $ = cheerio.load(html); + expect($('#address').text()).to.equal('0.0.0.0'); + }); + }); + + describe('Development', () => { + /** @type {import('./test-utils').DevServer} */ + let devServer; + + before(async () => { + devServer = await fixture.startDevServer(); + }); + + after(async () => { + await devServer.stop(); + }); + + it('Gets the address', async () => { + let res = await fixture.fetch('/'); + expect(res.status).to.equal(200); + let html = await res.text(); + let $ = cheerio.load(html); + let address = $('#address'); + + // Just checking that something is here. Not specifying address as it + // might differ per machine. + expect(address.length).to.be.greaterThan(0); + }); + }); + }); + + describe('SSR adapter not implemented', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/client-address/', + experimental: { + ssr: true, + }, + adapter: testAdapter({ provideAddress: false }), + }); + await fixture.build(); + }); + + it('Gets an error message', async () => { + const app = await fixture.loadTestAdapterApp(); + const request = new Request('http://example.com/'); + const response = await app.render(request); + expect(response.status).to.equal(500); + }); + }) + + describe('SSG', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/client-address/', + }); + }); + + describe('Build', () => { + it('throws during generation', async () => { + try { + await fixture.build(); + expect(false).to.equal(true, 'Build should not have completed'); + } catch(err) { + expect(err.message).to.match(/Astro\.clientAddress/, 'Error message mentions Astro.clientAddress'); + } + }) + }); + + describe('Development', () => { + /** @type {import('./test-utils').DevServer} */ + let devServer; + + before(async () => { + // We expect an error, so silence the output + const logging = { + dest: nodeLogDestination, + level: 'silent', + }; + devServer = await fixture.startDevServer({ logging }); + }); + + after(async () => { + await devServer.stop(); + }); + + it('is not accessible', async () => { + let res = await fixture.fetch('/'); + expect(res.status).to.equal(500); + }); + }) + }); +}); diff --git a/packages/astro/test/fixtures/client-address/src/pages/index.astro b/packages/astro/test/fixtures/client-address/src/pages/index.astro new file mode 100644 index 000000000..ea2cf65ca --- /dev/null +++ b/packages/astro/test/fixtures/client-address/src/pages/index.astro @@ -0,0 +1,12 @@ +--- +const address = Astro.clientAddress; +--- + + + Astro.clientAddress + + +

Astro.clientAddress

+
{ address }
+ + diff --git a/packages/astro/test/test-adapter.js b/packages/astro/test/test-adapter.js index 4b7eac527..297e117a2 100644 --- a/packages/astro/test/test-adapter.js +++ b/packages/astro/test/test-adapter.js @@ -4,7 +4,7 @@ import { viteID } from '../dist/core/util.js'; * * @returns {import('../src/@types/astro').AstroIntegration} */ -export default function () { +export default function ({ provideAddress } = { provideAddress: true }) { return { name: 'my-ssr-adapter', hooks: { @@ -23,7 +23,23 @@ export default function () { }, load(id) { if (id === '@my-ssr') { - return `import { App } from 'astro/app';export function createExports(manifest) { return { manifest, createApp: (streaming) => new App(manifest, streaming) }; }`; + return ` + import { App } from 'astro/app'; + + class MyApp extends App { + render(request) { + ${provideAddress ? `request[Symbol.for('astro.clientAddress')] = '0.0.0.0';` : ''} + return super.render(request); + } + } + + export function createExports(manifest) { + return { + manifest, + createApp: (streaming) => new MyApp(manifest, streaming) + }; + } + `; } }, }, diff --git a/packages/astro/test/test-utils.js b/packages/astro/test/test-utils.js index 346c2a2ac..ab43a94cc 100644 --- a/packages/astro/test/test-utils.js +++ b/packages/astro/test/test-utils.js @@ -127,6 +127,7 @@ export async function loadFixture(inlineConfig) { // Also do it on process exit, just in case. process.on('exit', resetAllFiles); + let fixtureId = new Date().valueOf(); let devServer; return { @@ -150,7 +151,7 @@ export async function loadFixture(inlineConfig) { await fs.promises.rm(config.outDir, { maxRetries: 10, recursive: true, force: true }); }, loadTestAdapterApp: async (streaming) => { - const url = new URL('./server/entry.mjs', config.outDir); + const url = new URL(`./server/entry.mjs?id=${fixtureId}`, config.outDir); const { createApp, manifest } = await import(url); const app = createApp(streaming); app.manifest = manifest; diff --git a/packages/integrations/cloudflare/src/server.ts b/packages/integrations/cloudflare/src/server.ts index 8e7f572c6..07858a7ed 100644 --- a/packages/integrations/cloudflare/src/server.ts +++ b/packages/integrations/cloudflare/src/server.ts @@ -20,6 +20,7 @@ export function createExports(manifest: SSRManifest) { } if (app.match(request)) { + Reflect.set(request, Symbol.for('astro.clientAddress'), request.headers.get('cf-connecting-ip')); return app.render(request); } diff --git a/packages/integrations/deno/src/server.ts b/packages/integrations/deno/src/server.ts index f6dbcb62c..b18e6034c 100644 --- a/packages/integrations/deno/src/server.ts +++ b/packages/integrations/deno/src/server.ts @@ -22,8 +22,10 @@ export function start(manifest: SSRManifest, options: Options) { const clientRoot = new URL('../client/', import.meta.url); const app = new App(manifest); - const handler = async (request: Request) => { + const handler = async (request: Request, connInfo: any) => { if (app.match(request)) { + let ip = connInfo?.remoteAddr?.hostname; + Reflect.set(request, Symbol.for('astro.clientAddress'), ip); return await app.render(request); } diff --git a/packages/integrations/netlify/src/netlify-edge-functions.ts b/packages/integrations/netlify/src/netlify-edge-functions.ts index 0d2974c61..a2c883585 100644 --- a/packages/integrations/netlify/src/netlify-edge-functions.ts +++ b/packages/integrations/netlify/src/netlify-edge-functions.ts @@ -1,6 +1,8 @@ import type { SSRManifest } from 'astro'; import { App } from 'astro/app'; +const clientAddressSymbol = Symbol.for('astro.clientAddress'); + export function createExports(manifest: SSRManifest) { const app = new App(manifest); @@ -13,6 +15,8 @@ export function createExports(manifest: SSRManifest) { return; } if (app.match(request)) { + const ip = request.headers.get('x-nf-client-connection-ip'); + Reflect.set(request, clientAddressSymbol, ip); return app.render(request); } diff --git a/packages/integrations/netlify/src/netlify-functions.ts b/packages/integrations/netlify/src/netlify-functions.ts index d342afc4c..0363fb803 100644 --- a/packages/integrations/netlify/src/netlify-functions.ts +++ b/packages/integrations/netlify/src/netlify-functions.ts @@ -15,6 +15,8 @@ function parseContentType(header?: string) { return header?.split(';')[0] ?? ''; } +const clientAddressSymbol = Symbol.for('astro.clientAddress'); + export const createExports = (manifest: SSRManifest, args: Args) => { const app = new App(manifest); @@ -71,6 +73,9 @@ export const createExports = (manifest: SSRManifest, args: Args) => { }; } + const ip = headers['x-nf-client-connection-ip']; + Reflect.set(request, clientAddressSymbol, ip); + const response: Response = await app.render(request); const responseHeaders = Object.fromEntries(response.headers.entries()); diff --git a/packages/integrations/vercel/src/edge/entrypoint.ts b/packages/integrations/vercel/src/edge/entrypoint.ts index 0cd069b6e..8063c271a 100644 --- a/packages/integrations/vercel/src/edge/entrypoint.ts +++ b/packages/integrations/vercel/src/edge/entrypoint.ts @@ -7,11 +7,14 @@ import './shim.js'; import type { SSRManifest } from 'astro'; import { App } from 'astro/app'; +const clientAddressSymbol = Symbol.for('astro.clientAddress'); + export function createExports(manifest: SSRManifest) { const app = new App(manifest); const handler = async (request: Request): Promise => { if (app.match(request)) { + Reflect.set(request, clientAddressSymbol, request.headers.get('x-forwarded-for')); return await app.render(request); } diff --git a/packages/integrations/vercel/src/serverless/request-transform.ts b/packages/integrations/vercel/src/serverless/request-transform.ts index e675045f9..6f3a063bd 100644 --- a/packages/integrations/vercel/src/serverless/request-transform.ts +++ b/packages/integrations/vercel/src/serverless/request-transform.ts @@ -1,6 +1,8 @@ import type { IncomingMessage, ServerResponse } from 'node:http'; import { Readable } from 'node:stream'; +const clientAddressSymbol = Symbol.for('astro.clientAddress'); + /* Credits to the SvelteKit team https://github.com/sveltejs/kit/blob/69913e9fda054fa6a62a80e2bb4ee7dca1005796/packages/kit/src/node.js @@ -66,11 +68,13 @@ export async function getRequest(base: string, req: IncomingMessage): Promise {