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 {