Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-https-http2-host-port.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudflare/vite-plugin": patch
---

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
29 changes: 28 additions & 1 deletion packages/vite-plugin-cloudflare/src/__tests__/utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,11 @@ describe("createRequestHandler", () => {
let httpServer: http.Server;
let port: number;
let capturedUrls: string[];
let capturedForwardedHosts: (string | null)[];

beforeEach(async () => {
capturedUrls = [];
capturedForwardedHosts = [];
});

afterEach(async () => {
Expand All @@ -117,13 +119,15 @@ describe("createRequestHandler", () => {
);
});

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

httpServer = http.createServer((req, res) => {
mutateReq?.(req);
void handler(
req as unknown as Parameters<typeof handler>[0],
res,
Expand Down Expand Up @@ -188,4 +192,27 @@ describe("createRequestHandler", () => {
});
expect(capturedUrls[0]).toBe(`http://127.0.0.1:${port}/path`);
});

test("preserves non-default port from `:authority` when `Host` is missing", async ({
expect,
}) => {
await startServer((req) => {
delete req.headers.host;
req.headers[":authority"] = "localhost:5173";
});
await fetch(`http://127.0.0.1:${port}/path`);
expect(capturedUrls[0]).toBe("http://localhost:5173/path");
expect(capturedForwardedHosts[0]).toBe("localhost:5173");
});

test("preserves non-default port from the `Host` header", async ({
expect,
}) => {
await startServer((req) => {
req.headers.host = "localhost:5173";
});
await fetch(`http://127.0.0.1:${port}/path`);
expect(capturedUrls[0]).toBe("http://localhost:5173/path");
expect(capturedForwardedHosts[0]).toBe("localhost:5173");
});
});
20 changes: 19 additions & 1 deletion packages/vite-plugin-cloudflare/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,25 @@ export function createRequestHandler(
// If the header is absent or invalid, `createRequest` falls back to the
// connection protocol (`req.socket.encrypted`).
const protocol = getForwardedProto(req);
request = createRequest(req, res, protocol ? { protocol } : undefined);
// Prefer Node host/:authority so HTTPS/HTTP2 keeps non-default ports (e.g. :5173).
// createHeaders() skips pseudo-headers, so Host can be missing and X-Forwarded-Host
// would otherwise not preserve the Vite origin port used by auth libraries (Clerk).
const nodeHost =
typeof req.headers.host === "string" ? req.headers.host : undefined;
const authority =
typeof req.headers[":authority"] === "string"
? req.headers[":authority"]
: undefined;
const host = nodeHost ?? authority;

request = createRequest(req, res, {
...(protocol ? { protocol } : {}),
...(host ? { host } : {}),
});

if (host && !request.headers.has("Host")) {
request.headers.set("Host", host);
}
Comment on lines +111 to +118

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 New host-resolution behaviour ships without any automated test

The new host/port resolution logic is added (createRequest call at packages/vite-plugin-cloudflare/src/utils.ts:111-118) with no accompanying unit test, even though the package already has a request-handler test harness that could exercise it, so the fix can silently regress.
Impact: The port-preserving behaviour could break again in a future change without anyone noticing.

Repository rule and existing test harness make a test feasible

CONTRIBUTING.md states "Every PR should include tests for the functionality that's being added". The PR marks "Automated tests not possible", but packages/vite-plugin-cloudflare/src/__tests__/utils.spec.ts:102-142 already sets up a real Node HTTP server and invokes createRequestHandler directly with a cast req, capturing request.url. A test can therefore assert host/port resolution by invoking the returned handler with a req whose headers contain only :authority (no host), verifying the captured URL keeps the non-default port and that X-Forwarded-Host is populated.

Prompt for agents
Add a unit test in packages/vite-plugin-cloudflare/src/__tests__/utils.spec.ts covering the new host resolution in createRequestHandler (packages/vite-plugin-cloudflare/src/utils.ts). The existing `createRequestHandler` describe block already spins up a real http server and casts `req`, so a test can either call the handler directly with a synthetic `req`/`res` pair whose headers contain only `:authority` (simulating HTTP/2 where `host` is absent), or mutate `req.headers` inside the server callback before delegating. Assert that the captured `request.url` keeps the non-default port from `:authority`, and add a case asserting the plain `host` header path still works.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems correct. Why are we setting the Host header again when we're already passing in ...(host ? { host } : {}), to createRequest @intrdx ?


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

Expand Down
Loading