From c21d84bfe74423ba588d2648af423accd34b79fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Wed, 8 Apr 2026 19:33:46 +0200 Subject: [PATCH 1/4] feat: add trustProxy option to respect X-Forwarded-* headers (#2660) Adds a `trustProxy` config option. When enabled, the request handler reads `X-Forwarded-Proto` and `X-Forwarded-Host` headers and rewrites `ctx.url` accordingly, so applications behind a reverse proxy get the correct protocol and hostname. Closes #2660 Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/fresh/src/app.ts | 15 +++++++++++++++ packages/fresh/src/config.ts | 12 ++++++++++++ packages/fresh/src/test_utils.ts | 1 + 3 files changed, 28 insertions(+) diff --git a/packages/fresh/src/app.ts b/packages/fresh/src/app.ts index fe1f740fb87..90aac96b7ce 100644 --- a/packages/fresh/src/app.ts +++ b/packages/fresh/src/app.ts @@ -190,6 +190,7 @@ export class App { root: ".", basePath: config.basePath ?? "", mode: config.mode ?? "production", + trustProxy: config.trustProxy ?? false, }; } @@ -392,6 +393,8 @@ export class App { this.#onError, ); + const trustProxy = this.config.trustProxy; + return async ( req: Request, conn: Deno.ServeHandlerInfo = DEFAULT_CONN_INFO, @@ -400,6 +403,18 @@ export class App { // Prevent open redirect attacks url.pathname = url.pathname.replace(/\/+/g, "/"); + // Apply X-Forwarded-* headers when behind a reverse proxy + if (trustProxy) { + const proto = req.headers.get("x-forwarded-proto"); + if (proto) { + url.protocol = proto + ":"; + } + const host = req.headers.get("x-forwarded-host"); + if (host) { + url.host = host; + } + } + const method = req.method.toUpperCase() as Method; const matched = router.match(method, url); let { params, pattern, item: handler, methodMatch } = matched; diff --git a/packages/fresh/src/config.ts b/packages/fresh/src/config.ts index 7fae4f086e9..0866eef5b57 100644 --- a/packages/fresh/src/config.ts +++ b/packages/fresh/src/config.ts @@ -11,6 +11,13 @@ export interface FreshConfig { * The mode Fresh can run in. */ mode?: "development" | "production"; + /** + * When enabled, Fresh will respect `X-Forwarded-Proto` and + * `X-Forwarded-Host` headers to construct `ctx.url`. Enable + * this when running behind a reverse proxy. + * @default false + */ + trustProxy?: boolean; } /** @@ -27,6 +34,11 @@ export interface ResolvedFreshConfig { * The mode Fresh can run in. */ mode: "development" | "production"; + /** + * When enabled, Fresh will respect `X-Forwarded-Proto` and + * `X-Forwarded-Host` headers to construct `ctx.url`. + */ + trustProxy: boolean; } export function parseDirPath( diff --git a/packages/fresh/src/test_utils.ts b/packages/fresh/src/test_utils.ts index c286b9a8819..ed6b6776238 100644 --- a/packages/fresh/src/test_utils.ts +++ b/packages/fresh/src/test_utils.ts @@ -67,6 +67,7 @@ const DEFAULT_CONFIG: ResolvedFreshConfig = { root: "", mode: "production", basePath: "", + trustProxy: false, }; export function serveMiddleware( From ea8f34f0a1ce88d5a0bfb7d2a89bdf025aed5437 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Wed, 8 Apr 2026 21:37:43 +0200 Subject: [PATCH 2/4] docs: document trustProxy configuration option Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/latest/concepts/app.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/latest/concepts/app.md b/docs/latest/concepts/app.md index 215fed0dcaa..2bd48df7b7c 100644 --- a/docs/latest/concepts/app.md +++ b/docs/latest/concepts/app.md @@ -37,6 +37,23 @@ With `basePath: "/my-app"`, a route registered at `/about` will respond to mounted alongside other apps. The base path is available in handlers via `ctx.config.basePath`. +### Reverse proxy support + +When running behind a reverse proxy (nginx, Caddy, etc.), set `trustProxy` to +make `ctx.url` reflect the client-facing URL instead of the internal one: + +```ts +const app = new App({ trustProxy: true }); +``` + +With this enabled, Fresh reads `X-Forwarded-Proto` and `X-Forwarded-Host` +headers and rewrites `ctx.url` accordingly. For example, if your proxy +terminates TLS and forwards `X-Forwarded-Proto: https`, `ctx.url.protocol` will +be `https:` instead of `http:`. + +> [warn]: Only enable `trustProxy` when your app is actually behind a trusted +> reverse proxy. Untrusted clients could otherwise spoof these headers. + All items are applied from top to bottom. This means that when you defined a middleware _after_ a `.get()` handler, it won't be included. From 25af10522738c23d69b05635836378778596acea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Thu, 9 Apr 2026 09:45:21 +0200 Subject: [PATCH 3/4] fix: add trustProxy to all ResolvedFreshConfig literals Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/fresh/src/config.ts | 3 ++- packages/fresh/src/middlewares/static_files_test.ts | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/fresh/src/config.ts b/packages/fresh/src/config.ts index 0866eef5b57..5f751c42245 100644 --- a/packages/fresh/src/config.ts +++ b/packages/fresh/src/config.ts @@ -36,7 +36,8 @@ export interface ResolvedFreshConfig { mode: "development" | "production"; /** * When enabled, Fresh will respect `X-Forwarded-Proto` and - * `X-Forwarded-Host` headers to construct `ctx.url`. + * `X-Forwarded-Host` headers to construct `ctx.url`. Enable + * this when running behind a reverse proxy. */ trustProxy: boolean; } diff --git a/packages/fresh/src/middlewares/static_files_test.ts b/packages/fresh/src/middlewares/static_files_test.ts index d131bccc799..3ba5daacb51 100644 --- a/packages/fresh/src/middlewares/static_files_test.ts +++ b/packages/fresh/src/middlewares/static_files_test.ts @@ -100,6 +100,7 @@ Deno.test("static files - etag in production", async () => { root: "", basePath: "", mode: "production", + trustProxy: false, }, }, ); @@ -134,6 +135,7 @@ Deno.test("static files - no etag in development", async () => { root: "", basePath: "", mode: "development", + trustProxy: false, }, }, ); @@ -191,6 +193,7 @@ Deno.test("static files - disables caching in development", async () => { root: "", basePath: "", mode: "development", + trustProxy: false, }, }, ); @@ -215,6 +218,7 @@ Deno.test("static files - enables caching in production", async () => { root: "", basePath: "", mode: "production", + trustProxy: false, }, }, ); From 3ed5148be5995e32b35ab98bcf1201c74a961049 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Thu, 9 Apr 2026 09:47:17 +0200 Subject: [PATCH 4/4] docs: add security warning to trustProxy JSDoc Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/fresh/src/config.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/fresh/src/config.ts b/packages/fresh/src/config.ts index 5f751c42245..7fa763c43bd 100644 --- a/packages/fresh/src/config.ts +++ b/packages/fresh/src/config.ts @@ -15,6 +15,9 @@ export interface FreshConfig { * When enabled, Fresh will respect `X-Forwarded-Proto` and * `X-Forwarded-Host` headers to construct `ctx.url`. Enable * this when running behind a reverse proxy. + * + * Only enable `trustProxy` when your app is actually behind a trusted + * reverse proxy. Untrusted clients could otherwise spoof these headers. * @default false */ trustProxy?: boolean; @@ -38,6 +41,9 @@ export interface ResolvedFreshConfig { * When enabled, Fresh will respect `X-Forwarded-Proto` and * `X-Forwarded-Host` headers to construct `ctx.url`. Enable * this when running behind a reverse proxy. + * + * Only enable `trustProxy` when your app is actually behind a trusted + * reverse proxy. Untrusted clients could otherwise spoof these headers. */ trustProxy: boolean; }