Skip to content

Commit 07878fc

Browse files
bartlomiejuclaude
andauthored
feat: add trustProxy option to respect X-Forwarded-* headers (#3757)
## Summary - Adds `trustProxy` option to `FreshConfig` (default: `false`) - When enabled, the handler reads `X-Forwarded-Proto` and `X-Forwarded-Host` headers and rewrites `ctx.url` so it reflects the actual client-facing URL - Useful for apps running behind nginx, Caddy, or other reverse proxies where `ctx.url` would otherwise return `http://` instead of `https://` Closes #2660 ## Usage ```ts const app = new App({ trustProxy: true }); ``` --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 990aeb0 commit 07878fc

5 files changed

Lines changed: 56 additions & 0 deletions

File tree

docs/latest/concepts/app.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,23 @@ With `basePath: "/my-app"`, a route registered at `/about` will respond to
3737
mounted alongside other apps. The base path is available in handlers via
3838
`ctx.config.basePath`.
3939

40+
### Reverse proxy support
41+
42+
When running behind a reverse proxy (nginx, Caddy, etc.), set `trustProxy` to
43+
make `ctx.url` reflect the client-facing URL instead of the internal one:
44+
45+
```ts
46+
const app = new App({ trustProxy: true });
47+
```
48+
49+
With this enabled, Fresh reads `X-Forwarded-Proto` and `X-Forwarded-Host`
50+
headers and rewrites `ctx.url` accordingly. For example, if your proxy
51+
terminates TLS and forwards `X-Forwarded-Proto: https`, `ctx.url.protocol` will
52+
be `https:` instead of `http:`.
53+
54+
> [warn]: Only enable `trustProxy` when your app is actually behind a trusted
55+
> reverse proxy. Untrusted clients could otherwise spoof these headers.
56+
4057
All items are applied from top to bottom. This means that when you defined a
4158
middleware _after_ a `.get()` handler, it won't be included.
4259

packages/fresh/src/app.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ export class App<State> {
190190
root: ".",
191191
basePath: config.basePath ?? "",
192192
mode: config.mode ?? "production",
193+
trustProxy: config.trustProxy ?? false,
193194
};
194195
}
195196

@@ -392,6 +393,8 @@ export class App<State> {
392393
this.#onError,
393394
);
394395

396+
const trustProxy = this.config.trustProxy;
397+
395398
return async (
396399
req: Request,
397400
conn: Deno.ServeHandlerInfo = DEFAULT_CONN_INFO,
@@ -400,6 +403,18 @@ export class App<State> {
400403
// Prevent open redirect attacks
401404
url.pathname = url.pathname.replace(/\/+/g, "/");
402405

406+
// Apply X-Forwarded-* headers when behind a reverse proxy
407+
if (trustProxy) {
408+
const proto = req.headers.get("x-forwarded-proto");
409+
if (proto) {
410+
url.protocol = proto + ":";
411+
}
412+
const host = req.headers.get("x-forwarded-host");
413+
if (host) {
414+
url.host = host;
415+
}
416+
}
417+
403418
const method = req.method.toUpperCase() as Method;
404419
const matched = router.match(method, url);
405420
let { params, pattern, item: handler, methodMatch } = matched;

packages/fresh/src/config.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,16 @@ export interface FreshConfig {
1111
* The mode Fresh can run in.
1212
*/
1313
mode?: "development" | "production";
14+
/**
15+
* When enabled, Fresh will respect `X-Forwarded-Proto` and
16+
* `X-Forwarded-Host` headers to construct `ctx.url`. Enable
17+
* this when running behind a reverse proxy.
18+
*
19+
* Only enable `trustProxy` when your app is actually behind a trusted
20+
* reverse proxy. Untrusted clients could otherwise spoof these headers.
21+
* @default false
22+
*/
23+
trustProxy?: boolean;
1424
}
1525

1626
/**
@@ -27,6 +37,15 @@ export interface ResolvedFreshConfig {
2737
* The mode Fresh can run in.
2838
*/
2939
mode: "development" | "production";
40+
/**
41+
* When enabled, Fresh will respect `X-Forwarded-Proto` and
42+
* `X-Forwarded-Host` headers to construct `ctx.url`. Enable
43+
* this when running behind a reverse proxy.
44+
*
45+
* Only enable `trustProxy` when your app is actually behind a trusted
46+
* reverse proxy. Untrusted clients could otherwise spoof these headers.
47+
*/
48+
trustProxy: boolean;
3049
}
3150

3251
export function parseDirPath(

packages/fresh/src/middlewares/static_files_test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ Deno.test("static files - etag in production", async () => {
100100
root: "",
101101
basePath: "",
102102
mode: "production",
103+
trustProxy: false,
103104
},
104105
},
105106
);
@@ -134,6 +135,7 @@ Deno.test("static files - no etag in development", async () => {
134135
root: "",
135136
basePath: "",
136137
mode: "development",
138+
trustProxy: false,
137139
},
138140
},
139141
);
@@ -191,6 +193,7 @@ Deno.test("static files - disables caching in development", async () => {
191193
root: "",
192194
basePath: "",
193195
mode: "development",
196+
trustProxy: false,
194197
},
195198
},
196199
);
@@ -215,6 +218,7 @@ Deno.test("static files - enables caching in production", async () => {
215218
root: "",
216219
basePath: "",
217220
mode: "production",
221+
trustProxy: false,
218222
},
219223
},
220224
);

packages/fresh/src/test_utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ const DEFAULT_CONFIG: ResolvedFreshConfig = {
6767
root: "",
6868
mode: "production",
6969
basePath: "",
70+
trustProxy: false,
7071
};
7172

7273
export function serveMiddleware<T>(

0 commit comments

Comments
 (0)