From 439da8fdd1be40fdbcf582b3f11adb91d61758a1 Mon Sep 17 00:00:00 2001 From: David Sherret Date: Sat, 27 Jun 2026 15:05:51 -0400 Subject: [PATCH] Fix CORS on redirect responses and add Vary: Origin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cross-origin fetches run the CORS check on every response in a redirect chain, so the 302 produced for non-wasm assets needs an Access-Control-Allow-Origin header too — without it the redirect was blocked before the final asset response (which did set the header) was reached. Also add `Vary: Origin` everywhere the header is emitted, since its value varies by request origin (dprint.dev / localhost / 127.0.0.1). --- handleRequest.test.ts | 2 ++ handleRequest.ts | 20 ++++++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/handleRequest.test.ts b/handleRequest.test.ts index 8d35299..14ef40f 100644 --- a/handleRequest.test.ts +++ b/handleRequest.test.ts @@ -335,6 +335,7 @@ it("should redirect non-wasm plugins to asset URL", async () => { expect(response.headers.get("location")).toEqual( "https://plugins.dprint.dev/dprint/dprint-plugin-prettier/0.7.0/asset/plugin.json", ); + expect(response.headers.get("access-control-allow-origin")).toEqual("https://dprint.dev"); }); it("should not redirect wasm plugins", async () => { @@ -354,6 +355,7 @@ it("should redirect non-allowed org asset to GitHub", async () => { expect(response.headers.get("location")).toEqual( "https://github.com/someone/some-repo/releases/download/0.1.0/file.zip", ); + expect(response.headers.get("access-control-allow-origin")).toEqual("https://dprint.dev"); }); it("should return 404 for asset not found", async () => { diff --git a/handleRequest.ts b/handleRequest.ts index 5006737..b737a3d 100644 --- a/handleRequest.ts +++ b/handleRequest.ts @@ -35,7 +35,7 @@ export function createRequestHandler() { const assetResult = tryResolveAssetUrl(url); if (assetResult != null) { if (!assetResult.shouldCache) { - return Response.redirect(assetResult.githubUrl, 302); + return createRedirectResponse(request, assetResult.githubUrl); } return servePlugin(request, assetResult.githubUrl, ctx); } @@ -47,7 +47,7 @@ export function createRequestHandler() { // plugin.json files resolve correctly const assetPath = githubUrlToAssetPath(githubUrl); if (assetPath != null) { - return Response.redirect(`${url.origin}${assetPath}`, 302); + return createRedirectResponse(request, `${url.origin}${assetPath}`); } } return servePlugin(request, githubUrl, ctx); @@ -124,6 +124,7 @@ export function createRequestHandler() { headers: { "content-type": result.contentType, "Access-Control-Allow-Origin": getAccessControlAllowOrigin(request), + "Vary": "Origin", }, status: result.status, }); @@ -209,6 +210,20 @@ export async function resolvePluginOrSchemaUrl(url: URL) { return await tryResolveSchemaUrl(url); } +// builds a redirect that also carries the CORS header, because for +// cross-origin fetches the browser runs the CORS check on the redirect +// response itself — not just the final response it points at +function createRedirectResponse(request: Request, location: string) { + return new Response(null, { + status: 302, + headers: { + "location": location, + "Access-Control-Allow-Origin": getAccessControlAllowOrigin(request), + "Vary": "Origin", + }, + }); +} + function getAccessControlAllowOrigin(request: Request) { const origin = request.headers.get("origin"); return origin != null && isLocalHostname(new URL(origin).hostname) @@ -225,6 +240,7 @@ function createJsonResponse(text: string, request: Request) { headers: { "content-type": contentTypes.json, "Access-Control-Allow-Origin": getAccessControlAllowOrigin(request), + "Vary": "Origin", }, status: 200, });