From a51d004ada86012e6289e612a98d03d00b3f3cc1 Mon Sep 17 00:00:00 2001 From: Luke Policinski Date: Mon, 12 Oct 2026 12:19:57 -0400 Subject: [PATCH 1/2] bug: the clip Worker counts a POST from the clip player as a view, and keeps one edge copy of a clip whether or not its request is marked noview --- .../backblaze-proxy/index.test.ts | 149 ++++++++++++++++++ cloudflare-workers/backblaze-proxy/index.ts | 48 +++++- 2 files changed, 195 insertions(+), 2 deletions(-) diff --git a/cloudflare-workers/backblaze-proxy/index.test.ts b/cloudflare-workers/backblaze-proxy/index.test.ts index 08e36ad..e64e409 100644 --- a/cloudflare-workers/backblaze-proxy/index.test.ts +++ b/cloudflare-workers/backblaze-proxy/index.test.ts @@ -166,3 +166,152 @@ describe("backblaze-proxy health", () => { }); }); + +describe("backblaze-proxy clip views", () => { + const API_URL = "https://api.5stack.gg"; + const CLIP = "clips/76561198000000001/0b7c.mp4"; + const UA = "Mozilla/5.0 (Macintosh) Chrome/141"; + + let waited: Promise[]; + let cacheKeys: string[]; + + function ctx() { + return { + waitUntil: (promise: Promise) => waited.push(promise), + passThroughOnException: () => {}, + } as any; + } + + function viewCalls() { + return upstream.mock.calls.filter((call) => + String(call.arguments[0]).startsWith(`${API_URL}/clip-views/play`), + ); + } + + async function send( + method: string, + path: string, + headers: Record = {}, + ) { + const response = await worker.fetch( + new Request(`https://cf.5stack.gg/${path}`, { + method, + headers: { "user-agent": UA, "cf-connecting-ip": "203.0.113.9", ...headers }, + }), + { ...env, API_URL }, + ctx(), + ); + await Promise.all(waited); + return response; + } + + beforeEach(() => { + waited = []; + cacheKeys = []; + (globalThis as any).caches = { + default: { + match: async (request: Request) => { + cacheKeys.push(request.url); + return undefined; + }, + put: async (request: Request) => { + cacheKeys.push(request.url); + }, + }, + }; + }); + + it("counts a played clip", async () => { + await send("GET", `${CLIP}?name=ace.mp4&v=1`); + + assert.equal(viewCalls().length, 1); + }); + + it("does not count a request marked noview", async () => { + await send("GET", `${CLIP}?name=ace.mp4&v=1&noview=1`); + + assert.equal(viewCalls().length, 0); + }); + + it("keeps one edge copy of a clip whether or not the request is marked noview", async () => { + await send("GET", `${CLIP}?name=ace.mp4&v=1&noview=1`); + const marked = [...cacheKeys]; + cacheKeys = []; + await send("GET", `${CLIP}?name=ace.mp4&v=1`); + + assert.deepEqual(marked, cacheKeys); + assert.ok(cacheKeys.every((key) => !key.includes("noview"))); + }); + + it("drops noview from the cache key without re-encoding the rest", async () => { + await send("GET", `${CLIP}?name=ace%20on%20B.mp4&noview=1&v=1`); + + assert.ok( + cacheKeys.every( + (key) => + key === + `https://cf.5stack.gg/${CLIP}?name=ace%20on%20B.mp4&v=1#origin=`, + ), + ); + }); + + it("still keeps a named download apart from the inline copy", async () => { + await send("GET", `${CLIP}?name=ace.mp4&v=1&dl=1`); + const download = [...cacheKeys]; + cacheKeys = []; + await send("GET", `${CLIP}?name=ace.mp4&v=1`); + + assert.notDeepEqual(download, cacheKeys); + }); + + it("counts a POST to a clip as the player saying it was watched", async () => { + const response = await send("POST", `${CLIP}?name=ace.mp4&v=1`, { + Origin: ORIGIN, + }); + + assert.equal(response.status, 204); + assert.equal(response.headers.get("Access-Control-Allow-Origin"), ORIGIN); + const [call] = viewCalls(); + assert.ok(call); + const init = call.arguments[1] as RequestInit; + assert.equal( + (init.headers as Record).authorization, + "Bearer secret", + ); + const body = JSON.parse(String(init.body)); + assert.equal(body.file, CLIP); + assert.match(body.clientKey, /^[a-f0-9]{32}$/); + }); + + it("gives a POST and a GET from the same viewer one dedupe key", async () => { + await send("POST", CLIP); + await send("GET", CLIP); + + const keys = viewCalls().map( + (call) => JSON.parse(String((call.arguments[1] as RequestInit).body)).clientKey, + ); + assert.equal(keys.length, 2); + assert.equal(keys[0], keys[1]); + }); + + it("never fetches the clip itself for a POST", async () => { + await send("POST", CLIP); + + assert.equal(upstream.mock.callCount(), viewCalls().length); + assert.deepEqual(cacheKeys, []); + }); + + it("does not count a POST from a bot", async () => { + const response = await send("POST", CLIP, { "user-agent": "Discordbot/2.0" }); + + assert.equal(response.status, 204); + assert.equal(viewCalls().length, 0); + }); + + it("refuses a POST to anything that isn't a clip", async () => { + const response = await send("POST", "maps/latest.json"); + + assert.equal(response.status, 405); + assert.equal(viewCalls().length, 0); + }); +}); diff --git a/cloudflare-workers/backblaze-proxy/index.ts b/cloudflare-workers/backblaze-proxy/index.ts index 4dc1cfd..6dbfe95 100644 --- a/cloudflare-workers/backblaze-proxy/index.ts +++ b/cloudflare-workers/backblaze-proxy/index.ts @@ -235,6 +235,46 @@ function registerView( ); } +// The clip player's own report that a clip was watched. It plays from a +// `noview` URL so warming the next clip is never counted, then POSTs to the +// clip once playback passes the middle. Same viewer key, so a POST and a +// counted GET from one viewer dedupe together. +function viewBeacon( + request: Request, + env: { S3_SECRET: string; API_URL?: string }, + ctx: ExecutionContext, + reqOrigin: string | null, +): Response { + const key = resolveKey(new URL(request.url)); + if (!key || !/^clips\/.+\.mp4$/.test(key)) { + return new Response(null, { + status: 405, + statusText: "Method Not Allowed", + headers: corsHeaders(reqOrigin), + }); + } + if (env.API_URL && !BOT_UA.test(request.headers.get("user-agent") ?? "")) { + registerView(request, key, env, ctx); + } + return new Response(null, { status: 204, headers: corsHeaders(reqOrigin) }); +} + +// `noview` only decides whether the request is counted, so it stays out of +// the cache key: the player's marked URL and every unmarked one share the +// single copy fetched from B2. Filtered on the raw query so the other +// parameters keep the exact encoding existing cache entries were stored under. +function cacheableUrl(url: URL): string { + if (!url.searchParams.has("noview")) { + return url.toString(); + } + const query = url.search + .slice(1) + .split("&") + .filter((part) => part.split("=")[0] !== "noview") + .join("&"); + return `${url.origin}${url.pathname}${query ? `?${query}` : ""}`; +} + // Decorates a response on its way to the client: mp4 Content-Type fix and the // view beacon. The body is never read or rewritten here. function clientResponse( @@ -317,6 +357,10 @@ export default { return handleUpload(request, env, reqOrigin); } + if (request.method === "POST") { + return viewBeacon(request, env, ctx, reqOrigin); + } + if (!["GET", "HEAD"].includes(request.method)) { return new Response(null, { status: 405, @@ -344,7 +388,7 @@ export default { // Origin is in the cache key so each caller gets its own // Access-Control-Allow-Origin (credentialed fetches can't take `*`). const cache = caches.default; - const cacheUrl = `${request.url}#origin=${reqOrigin ?? ""}`; + const cacheUrl = `${cacheableUrl(url)}#origin=${reqOrigin ?? ""}`; const cacheKey = new Request(cacheUrl, { method: "GET" }); const isHead = request.method === "HEAD"; const lookupKey = rangeHeader @@ -648,7 +692,7 @@ function corsHeaders(reqOrigin: string | null): Record { const allowOrigin = reqOrigin ?? "*"; const headers: Record = { "Access-Control-Allow-Origin": allowOrigin, - "Access-Control-Allow-Methods": "GET, HEAD, PUT, OPTIONS", + "Access-Control-Allow-Methods": "GET, HEAD, POST, PUT, OPTIONS", "Access-Control-Allow-Headers": "Range, If-Range, Content-Type, Content-Length", "Access-Control-Expose-Headers": From 2662dd1cbd4f6495a9c9ad9033713991faaf7032 Mon Sep 17 00:00:00 2001 From: Luke Policinski Date: Mon, 12 Oct 2026 14:00:08 -0400 Subject: [PATCH 2/2] bug: the clip Worker no longer takes a POST as a view; the clip player's warm-up stays noview and shares the play's edge copy --- .../backblaze-proxy/index.test.ts | 51 ++++--------------- cloudflare-workers/backblaze-proxy/index.ts | 37 ++------------ 2 files changed, 15 insertions(+), 73 deletions(-) diff --git a/cloudflare-workers/backblaze-proxy/index.test.ts b/cloudflare-workers/backblaze-proxy/index.test.ts index e64e409..2c5dc94 100644 --- a/cloudflare-workers/backblaze-proxy/index.test.ts +++ b/cloudflare-workers/backblaze-proxy/index.test.ts @@ -264,54 +264,23 @@ describe("backblaze-proxy clip views", () => { assert.notDeepEqual(download, cacheKeys); }); - it("counts a POST to a clip as the player saying it was watched", async () => { + it("refuses a POST to a clip without counting or fetching it", async () => { const response = await send("POST", `${CLIP}?name=ace.mp4&v=1`, { Origin: ORIGIN, }); - assert.equal(response.status, 204); - assert.equal(response.headers.get("Access-Control-Allow-Origin"), ORIGIN); - const [call] = viewCalls(); - assert.ok(call); - const init = call.arguments[1] as RequestInit; - assert.equal( - (init.headers as Record).authorization, - "Bearer secret", - ); - const body = JSON.parse(String(init.body)); - assert.equal(body.file, CLIP); - assert.match(body.clientKey, /^[a-f0-9]{32}$/); - }); - - it("gives a POST and a GET from the same viewer one dedupe key", async () => { - await send("POST", CLIP); - await send("GET", CLIP); - - const keys = viewCalls().map( - (call) => JSON.parse(String((call.arguments[1] as RequestInit).body)).clientKey, - ); - assert.equal(keys.length, 2); - assert.equal(keys[0], keys[1]); - }); - - it("never fetches the clip itself for a POST", async () => { - await send("POST", CLIP); - - assert.equal(upstream.mock.callCount(), viewCalls().length); - assert.deepEqual(cacheKeys, []); - }); - - it("does not count a POST from a bot", async () => { - const response = await send("POST", CLIP, { "user-agent": "Discordbot/2.0" }); - - assert.equal(response.status, 204); + assert.equal(response.status, 405); assert.equal(viewCalls().length, 0); + assert.equal(upstream.mock.callCount(), 0); + assert.deepEqual(cacheKeys, []); }); - it("refuses a POST to anything that isn't a clip", async () => { - const response = await send("POST", "maps/latest.json"); + it("does not offer POST to a browser", async () => { + const response = await send("OPTIONS", CLIP, { Origin: ORIGIN }); - assert.equal(response.status, 405); - assert.equal(viewCalls().length, 0); + assert.doesNotMatch( + response.headers.get("Access-Control-Allow-Methods") ?? "", + /POST/, + ); }); }); diff --git a/cloudflare-workers/backblaze-proxy/index.ts b/cloudflare-workers/backblaze-proxy/index.ts index 6dbfe95..b38bd6d 100644 --- a/cloudflare-workers/backblaze-proxy/index.ts +++ b/cloudflare-workers/backblaze-proxy/index.ts @@ -235,34 +235,11 @@ function registerView( ); } -// The clip player's own report that a clip was watched. It plays from a -// `noview` URL so warming the next clip is never counted, then POSTs to the -// clip once playback passes the middle. Same viewer key, so a POST and a -// counted GET from one viewer dedupe together. -function viewBeacon( - request: Request, - env: { S3_SECRET: string; API_URL?: string }, - ctx: ExecutionContext, - reqOrigin: string | null, -): Response { - const key = resolveKey(new URL(request.url)); - if (!key || !/^clips\/.+\.mp4$/.test(key)) { - return new Response(null, { - status: 405, - statusText: "Method Not Allowed", - headers: corsHeaders(reqOrigin), - }); - } - if (env.API_URL && !BOT_UA.test(request.headers.get("user-agent") ?? "")) { - registerView(request, key, env, ctx); - } - return new Response(null, { status: 204, headers: corsHeaders(reqOrigin) }); -} - // `noview` only decides whether the request is counted, so it stays out of -// the cache key: the player's marked URL and every unmarked one share the -// single copy fetched from B2. Filtered on the raw query so the other -// parameters keep the exact encoding existing cache entries were stored under. +// the cache key: the clip player's marked warm-up of the next clip and the +// unmarked play share the single copy fetched from B2. Filtered on the raw +// query so the other parameters keep the exact encoding existing cache +// entries were stored under. function cacheableUrl(url: URL): string { if (!url.searchParams.has("noview")) { return url.toString(); @@ -357,10 +334,6 @@ export default { return handleUpload(request, env, reqOrigin); } - if (request.method === "POST") { - return viewBeacon(request, env, ctx, reqOrigin); - } - if (!["GET", "HEAD"].includes(request.method)) { return new Response(null, { status: 405, @@ -692,7 +665,7 @@ function corsHeaders(reqOrigin: string | null): Record { const allowOrigin = reqOrigin ?? "*"; const headers: Record = { "Access-Control-Allow-Origin": allowOrigin, - "Access-Control-Allow-Methods": "GET, HEAD, POST, PUT, OPTIONS", + "Access-Control-Allow-Methods": "GET, HEAD, PUT, OPTIONS", "Access-Control-Allow-Headers": "Range, If-Range, Content-Type, Content-Length", "Access-Control-Expose-Headers":