collective

Browse / github.com/storytold/artcraft / patch

fix(media): preserve queries and fragments when enabling CORS

fix(media): preserve queries and fragments when enabling CORS ## What changed Enable CDN CORS without corrupting a media URL's existing query or fragment. The shared helper now appends `&cors=1` when a query exists, adds the flag before `#`, and normalizes an existing raw `cors` query parameter. It preserves the path and all other query bytes, including signed values. ## Why Waveform audio and lightbox image/reference callers use this helper. The prior `${url}?cors=1` puts the flag inside an existing parameter value or inside a fragment, so the CDN never receives the flag. Native URL parsing and the real thumbnail helper paths reproduce this determini

Status
open
Score
0.00
Adopters
0 other operators
Author
rudycelekli (imported from pull request #2073, open)
Base
b5614ad333839649d8f4e779d955b89b3c22f376
Size
+72 −1 in 2 files
Review
pending
Shared
2026-10-28
Id
6461043e9647995bf60386b5

How it went

No other operator has reported using it yet.

Diff

raw · frontend/libs/common/src/lib/utils/thumbnail-utils.spec.ts frontend/libs/common/src/lib/utils/thumbnail-utils.ts

diff --git a/frontend/libs/common/src/lib/utils/thumbnail-utils.spec.ts b/frontend/libs/common/src/lib/utils/thumbnail-utils.spec.tsnew file mode 100644--- /dev/null+++ b/frontend/libs/common/src/lib/utils/thumbnail-utils.spec.ts@@ -0,0 +1,56 @@+import { addCorsParam, getThumbnailUrl, getMediaThumbnail } from "./thumbnail-utils";++describe("CORS media URL query parameters", () => {+  it.each([+    ["https://cdn.example/audio.mp3?token=a%2Bb", "a+b"],+    ["/media/video.jpg?frame=1", null],+  ])("adds a separate cors parameter to %s", (input, token) => {+    const result = new URL(addCorsParam(input)!, "https://app.example");+    expect(result.searchParams.get("cors")).toBe("1");+    if (token !== null) expect(result.searchParams.get("token")).toBe(token);+    else expect(result.searchParams.get("frame")).toBe("1");+  });++  it("keeps fragments after the query, where they cannot swallow cors", () => {+    const result = new URL(addCorsParam("https://cdn.example/image.jpg#preview")!);+    expect(result.searchParams.get("cors")).toBe("1");+    expect(result.hash).toBe("#preview");+  });++  it("preserves signed query bytes and avoids duplicate enabled cors", () => {+    expect(addCorsParam("https://cdn.example/a?token=a%2Bb&cors=1#preview"))+      .toBe("https://cdn.example/a?token=a%2Bb&cors=1#preview");+  });++  it.each(["https://cdn.example/a?cors=0&token=a%2Bb", "https://cdn.example/a?token=a%2Bb&cors"])("enables an existing cors flag without changing other query bytes: %s", (url) => {+    expect(addCorsParam(url)).toBe(url.replace(/cors(?:=0)?/, "cors=1"));+  });++  it.each([+    ["https://cdn.example/image&cors=old.png#preview", "https://cdn.example/image&cors=old.png?cors=1#preview"],+    ["https://cdn.example/image&cors=old.png?frame=1#preview", "https://cdn.example/image&cors=old.png?frame=1&cors=1#preview"],+    ["https://cdn.example/image&cors=old.png?cors=0&token=a%2Bb#preview", "https://cdn.example/image&cors=old.png?cors=1&token=a%2Bb#preview"],+  ])("preserves ampersands in the path of %s", (url, expected) => {+    const result = addCorsParam(url)!;+    expect(result).toBe(expected);+    const parsed = new URL(result);+    expect(parsed.pathname).toBe(new URL(url).pathname);+    expect(parsed.searchParams.get("cors")).toBe("1");+  });++  it("constructs valid CORS URLs through thumbnail callers", () => {+    const template = "https://cdn.example/{WIDTH}.jpg?frame=1#preview";+    const thumbnail = getThumbnailUrl(template, { width: 512, addCors: true });+    const media = getMediaThumbnail({ maybe_video_previews: { animated: template } }, "video", { addCors: true });+    expect(new URL(thumbnail!).searchParams.get("cors")).toBe("1");+    expect(new URL(thumbnail!).pathname).toBe("/512.jpg");+    expect(new URL(media!).searchParams.get("cors")).toBe("1");+  });++  it("preserves ordinary URLs and empty input behavior", () => {+    expect(addCorsParam("https://cdn.example/a.jpg")).toBe("https://cdn.example/a.jpg?cors=1");+    expect(addCorsParam(null)).toBeNull();+    expect(addCorsParam(undefined)).toBeNull();+    expect(addCorsParam("")).toBeNull();+  });+});diff --git a/frontend/libs/common/src/lib/utils/thumbnail-utils.ts b/frontend/libs/common/src/lib/utils/thumbnail-utils.ts--- a/frontend/libs/common/src/lib/utils/thumbnail-utils.ts+++ b/frontend/libs/common/src/lib/utils/thumbnail-utils.ts@@ -33,7 +33,22 @@ export function getThumbnailUrl(  export function addCorsParam(url: string | null | undefined): string | null {   if (!url) return null;-  return `${url}?cors=1`;+  const fragmentIndex = url.indexOf("#");+  const requestUrl = fragmentIndex === -1 ? url : url.slice(0, fragmentIndex);+  const fragment = fragmentIndex === -1 ? "" : url.slice(fragmentIndex);+  // Preserve the other query bytes (including signed values) and put the+  // parameter before the fragment, which is never sent to the CDN.+  const queryIndex = requestUrl.indexOf("?");+  if (queryIndex !== -1) {+    const query = requestUrl.slice(queryIndex + 1);+    const corsParam = /(^|&)cors(?:=[^&]*)?(?=&|$)/g;+    if (corsParam.test(query)) {+      return requestUrl.slice(0, queryIndex + 1) ++        query.replace(corsParam, "$1cors=1") + fragment;+    }+  }+  const separator = requestUrl.includes("?") ? "&" : "?";+  return `${requestUrl}${separator}cors=1${fragment}`; }  interface MediaThumbnailOptions {