Skip to content

Commit 7db27e5

Browse files
intrdxcursoragent
andcommitted
fix: preserve HTTPS/HTTP2 host port in vite-plugin requests
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 48f0c6c commit 7db27e5

3 files changed

Lines changed: 52 additions & 2 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@cloudflare/vite-plugin": patch
3+
---
4+
5+
Preserve the host and non-default port (e.g. `localhost:5173`) when the Vite dev server runs over HTTPS/HTTP2, so authentication flows such as Clerk no longer redirect-loop to the wrong origin

packages/vite-plugin-cloudflare/src/__tests__/utils.spec.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,11 @@ describe("createRequestHandler", () => {
106106
let httpServer: http.Server;
107107
let port: number;
108108
let capturedUrls: string[];
109+
let capturedForwardedHosts: (string | null)[];
109110

110111
beforeEach(async () => {
111112
capturedUrls = [];
113+
capturedForwardedHosts = [];
112114
});
113115

114116
afterEach(async () => {
@@ -117,13 +119,15 @@ describe("createRequestHandler", () => {
117119
);
118120
});
119121

120-
function startServer() {
122+
function startServer(mutateReq?: (req: http.IncomingMessage) => void) {
121123
const handler = createRequestHandler(async (request) => {
122124
capturedUrls.push(request.url);
125+
capturedForwardedHosts.push(request.headers.get("X-Forwarded-Host"));
123126
return new MiniflareResponse("OK");
124127
});
125128

126129
httpServer = http.createServer((req, res) => {
130+
mutateReq?.(req);
127131
void handler(
128132
req as unknown as Parameters<typeof handler>[0],
129133
res,
@@ -188,4 +192,27 @@ describe("createRequestHandler", () => {
188192
});
189193
expect(capturedUrls[0]).toBe(`http://127.0.0.1:${port}/path`);
190194
});
195+
196+
test("preserves non-default port from `:authority` when `Host` is missing", async ({
197+
expect,
198+
}) => {
199+
await startServer((req) => {
200+
delete req.headers.host;
201+
req.headers[":authority"] = "localhost:5173";
202+
});
203+
await fetch(`http://127.0.0.1:${port}/path`);
204+
expect(capturedUrls[0]).toBe("http://localhost:5173/path");
205+
expect(capturedForwardedHosts[0]).toBe("localhost:5173");
206+
});
207+
208+
test("preserves non-default port from the `Host` header", async ({
209+
expect,
210+
}) => {
211+
await startServer((req) => {
212+
req.headers.host = "localhost:5173";
213+
});
214+
await fetch(`http://127.0.0.1:${port}/path`);
215+
expect(capturedUrls[0]).toBe("http://localhost:5173/path");
216+
expect(capturedForwardedHosts[0]).toBe("localhost:5173");
217+
});
191218
});

packages/vite-plugin-cloudflare/src/utils.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,25 @@ export function createRequestHandler(
9797
// If the header is absent or invalid, `createRequest` falls back to the
9898
// connection protocol (`req.socket.encrypted`).
9999
const protocol = getForwardedProto(req);
100-
request = createRequest(req, res, protocol ? { protocol } : undefined);
100+
// Prefer Node host/:authority so HTTPS/HTTP2 keeps non-default ports (e.g. :5173).
101+
// createHeaders() skips pseudo-headers, so Host can be missing and X-Forwarded-Host
102+
// would otherwise not preserve the Vite origin port used by auth libraries (Clerk).
103+
const nodeHost =
104+
typeof req.headers.host === "string" ? req.headers.host : undefined;
105+
const authority =
106+
typeof req.headers[":authority"] === "string"
107+
? req.headers[":authority"]
108+
: undefined;
109+
const host = nodeHost ?? authority;
110+
111+
request = createRequest(req, res, {
112+
...(protocol ? { protocol } : {}),
113+
...(host ? { host } : {}),
114+
});
115+
116+
if (host && !request.headers.has("Host")) {
117+
request.headers.set("Host", host);
118+
}
101119

102120
let response = await handler(toMiniflareRequest(request), req);
103121

0 commit comments

Comments
 (0)