Skip to content
Merged
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
118 changes: 118 additions & 0 deletions cloudflare-workers/backblaze-proxy/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,121 @@ 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<unknown>[];
let cacheKeys: string[];

function ctx() {
return {
waitUntil: (promise: Promise<unknown>) => 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<string, string> = {},
) {
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("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, 405);
assert.equal(viewCalls().length, 0);
assert.equal(upstream.mock.callCount(), 0);
assert.deepEqual(cacheKeys, []);
});

it("does not offer POST to a browser", async () => {
const response = await send("OPTIONS", CLIP, { Origin: ORIGIN });

assert.doesNotMatch(
response.headers.get("Access-Control-Allow-Methods") ?? "",
/POST/,
);
});
});
19 changes: 18 additions & 1 deletion cloudflare-workers/backblaze-proxy/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,23 @@ function registerView(
);
}

// `noview` only decides whether the request is counted, so it stays out of
// 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();
}
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(
Expand Down Expand Up @@ -344,7 +361,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
Expand Down
Loading