Public: anyone can read this; the crew can change it.

GET /api/v1/version carries an ETag and answers a repeat with 304; a harness probing it on every connect pays a header #9257

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

mason/forge:mason/9251-get-api-v1-version-carries-an-etag-and-a → main 2 commits fork

  • Merged. mason/9251-get-api-v1-version-carries-an-etag-and-a was merged into main as 425aaf9.

Review

Sign in to review this change.
2 files changed +81 −1
src/http/api/forge_origin.ts +8 −1
@@ -53,7 +53,14 @@ function whole(raw: string | null, fallback: number, max: number): number {
5353 export function forgeOriginRoutes(router: Router<Handler>): void {
5454 router.add("GET", "/api/v1/version", (ctx) => {
5555 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
56 return timePhase(ctx.request, "render", () => json({ version: FORGE_API_VERSION, software: "forge" }));
56 return timePhase(ctx.request, "render", () => {
57 // The same body for everyone, so the tag is derived from it alone.
58 const text = JSON.stringify({ version: FORGE_API_VERSION, software: "forge" });
59 const etag = weakEtag(text);
60 const headers = { etag, "cache-control": "no-cache" };
61 if (holds(ctx.request, etag)) return new Response(null, { status: 304, headers });
62 return new Response(text, { headers: { "content-type": "application/json", ...headers } });
63 });
5764 });
5865
5966 // The signed-in account's own repositories, `page` and `limit` as Gitea
test/version-etag.test.ts added +73
@@ -0,0 +1,73 @@
1 /** GET /api/v1/version carries an ETag; a repeat is a 304 until FORGE_API_VERSION changes. */
2 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
3 import { mkdtemp, rm } from "node:fs/promises";
4 import { tmpdir } from "node:os";
5 import { join } from "node:path";
6 import { createApp, type App } from "../src/app.ts";
7 import { createToken } from "../src/core/tokens.ts";
8 import { createUser } from "../src/core/users.ts";
9 import { FORGE_API_VERSION } from "../src/http/api/forge_origin.ts";
10 import { weakEtag } from "../src/http/etag.ts";
11 import { serve, type Servers } from "../src/http/server.ts";
12
13 describe("version ETag", () => {
14 let dataDir: string;
15 let app: App;
16 let servers: Servers;
17 let token: string;
18 const V = "/api/v1/version";
19 const body = JSON.stringify({ version: FORGE_API_VERSION, software: "forge" });
20 const expected = weakEtag(body);
21
22 const get = (extra: Record<string, string> = {}) =>
23 fetch(`${servers.url}${V}`, { headers: extra });
24
25 beforeAll(async () => {
26 dataDir = await mkdtemp(join(tmpdir(), "forge-version-etag-"));
27 app = createApp({ dataDir, port: 0 });
28 servers = await serve(app);
29 const u = await createUser(app.db, { origin: "seed", trusted: true, username: "alice", email: "[email protected]", password: "a-good-passphrase" });
30 if (!u.ok) throw new Error(u.error);
31 token = createToken(app.db, u.value.id, "admin", "admin").token;
32 }, 30_000);
33
34 afterAll(async () => {
35 servers.stop();
36 app.close();
37 await rm(dataDir, { recursive: true, force: true });
38 });
39
40 test("200 with a weak tag derived from the body, no-cache, no vary", async () => {
41 const res = await get();
42 expect(res.status).toBe(200);
43 expect(res.headers.get("etag")).toBe(expected);
44 expect(res.headers.get("cache-control")).toBe("no-cache");
45 expect(res.headers.get("vary") ?? "").not.toMatch(/authorization|cookie/i);
46 expect(await res.text()).toBe(body);
47 });
48
49 test("a repeat is a 304 with the same headers, no body and the security floor", async () => {
50 const res = await get({ "if-none-match": expected });
51 expect(res.status).toBe(304);
52 expect(res.headers.get("etag")).toBe(expected);
53 expect(res.headers.get("cache-control")).toBe("no-cache");
54 expect(res.headers.get("vary") ?? "").not.toMatch(/authorization|cookie/i);
55 expect(res.headers.get("x-content-type-options")).toBe("nosniff");
56 expect(res.headers.get("referrer-policy")).not.toBeNull();
57 expect(await res.text()).toBe("");
58 expect((await get({ "if-none-match": "*" })).status).toBe(304);
59 });
60
61 test("a signed-in and an anonymous caller get the same tag", async () => {
62 const anon = await get();
63 const user = await get({ authorization: `Bearer ${token}` });
64 expect(user.headers.get("etag")).toBe(anon.headers.get("etag"));
65 expect((await get({ authorization: `Bearer ${token}`, "if-none-match": expected })).status).toBe(304);
66 });
67
68 test("a stale tag gets a 200 with the body", async () => {
69 const res = await get({ "if-none-match": weakEtag("{}") });
70 expect(res.status).toBe(200);
71 expect(await res.text()).toBe(body);
72 });
73 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs