summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--workers/branch/src/index.ts11
-rw-r--r--workers/branch/test/content.test.ts95
-rw-r--r--workers/branch/wrangler.legacy.jsonc2
-rw-r--r--workers/branch/wrangler.rolling.jsonc2
-rw-r--r--workers/branch/wrangler.v14.jsonc2
-rw-r--r--workers/branch/wrangler.v15.jsonc2
6 files changed, 109 insertions, 5 deletions
diff --git a/workers/branch/src/index.ts b/workers/branch/src/index.ts
index d955d166..e9342c26 100644
--- a/workers/branch/src/index.ts
+++ b/workers/branch/src/index.ts
@@ -38,7 +38,16 @@ export function withDocsHeaders(
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
- const resp = await env.ASSETS.fetch(request);
+ // With assets html_handling "none", the runtime serves explicit .html URLs directly
+ // but does NOT map a directory URL ("/foo/") to its index.html — the worker must map
+ // trailing-slash URLs to index.html itself to preserve ReadTheDocs URL parity.
+ let assetRequest = request;
+ if (url.pathname.endsWith("/")) {
+ const mapped = new URL(url);
+ mapped.pathname = url.pathname + "index.html";
+ assetRequest = new Request(mapped, request);
+ }
+ const resp = await env.ASSETS.fetch(assetRequest);
return withDocsHeaders(resp, url.pathname, env);
},
} satisfies ExportedHandler<Env>;
diff --git a/workers/branch/test/content.test.ts b/workers/branch/test/content.test.ts
index fcf068da..807b1dd1 100644
--- a/workers/branch/test/content.test.ts
+++ b/workers/branch/test/content.test.ts
@@ -78,3 +78,98 @@ describe("default fetch entrypoint", () => {
expect(resp.headers.get("Cache-Control")).toBe("no-store");
});
});
+
+describe("directory-index mapping (html_handling \"none\", §3.2.3 amended 2026-07-22)", () => {
+ // Assets binding stub emulating html_handling:"none": exact-path lookups only —
+ // no extension inference and no directory auto-index. A bare "/foo/" therefore only
+ // resolves because the worker rewrites it to "/foo/index.html" before this runs, which
+ // is exactly the RTD-parity behavior under test.
+ const ASSET_MAP: Record<string, string> = {
+ "/en/rolling/index.html": "<html>root index</html>",
+ "/en/rolling/cli.html": "<html>cli page</html>",
+ "/en/rolling/guide/index.html": "<html>guide index</html>",
+ };
+
+ const makeAssetsEnv = (docsEnv: Env["DOCS_ENV"], seen: string[]): Env => ({
+ ASSETS: {
+ fetch: async (req: Request) => {
+ seen.push(req.url);
+ const body = ASSET_MAP[new URL(req.url).pathname];
+ return body === undefined
+ ? new Response("not found", { status: 404, headers: { "content-type": "text/html" } })
+ : new Response(body, { headers: { "content-type": "text/html" } });
+ },
+ } as unknown as Fetcher,
+ DOCS_BUILD_SHA: "testsha",
+ DOCS_ENV: docsEnv,
+ });
+
+ it("trailing-slash directory URL is mapped to index.html → 200 + index content", async () => {
+ const seen: string[] = [];
+ const resp = await worker.fetch(
+ new Request("https://docs.vyos.io/en/rolling/"),
+ makeAssetsEnv("production", seen),
+ );
+ expect(resp.status).toBe(200);
+ expect(await resp.text()).toBe("<html>root index</html>");
+ // worker rewrote "/en/rolling/" → "/en/rolling/index.html" before hitting ASSETS
+ expect(seen).toEqual(["https://docs.vyos.io/en/rolling/index.html"]);
+ // 200 still carries the build stamp + the page cache class
+ expect(resp.headers.get("X-Docs-Build")).toBe("testsha");
+ expect(resp.headers.get("Cache-Control"))
+ .toBe("public, max-age=0, s-maxage=300, must-revalidate");
+ });
+
+ it("explicit .html URL is served directly — 200, never a 3xx redirect", async () => {
+ const seen: string[] = [];
+ const resp = await worker.fetch(
+ new Request("https://docs.vyos.io/en/rolling/cli.html"),
+ makeAssetsEnv("production", seen),
+ );
+ expect(resp.status).toBe(200);
+ expect(resp.status).toBeLessThan(300); // no 307/308 for explicit .html paths
+ expect(await resp.text()).toBe("<html>cli page</html>");
+ // passed through unmodified — the worker never rewrites explicit-file paths
+ expect(seen).toEqual(["https://docs.vyos.io/en/rolling/cli.html"]);
+ expect(resp.headers.get("X-Docs-Build")).toBe("testsha");
+ expect(resp.headers.get("Cache-Control"))
+ .toBe("public, max-age=0, s-maxage=300, must-revalidate");
+ });
+
+ it("explicit nested /folder/index.html is served directly → 200", async () => {
+ const seen: string[] = [];
+ const resp = await worker.fetch(
+ new Request("https://docs.vyos.io/en/rolling/guide/index.html"),
+ makeAssetsEnv("production", seen),
+ );
+ expect(resp.status).toBe(200);
+ expect(await resp.text()).toBe("<html>guide index</html>");
+ expect(seen).toEqual(["https://docs.vyos.io/en/rolling/guide/index.html"]);
+ expect(resp.headers.get("X-Docs-Build")).toBe("testsha");
+ expect(resp.headers.get("Cache-Control"))
+ .toBe("public, max-age=0, s-maxage=300, must-revalidate");
+ });
+
+ it("extensionless page path (no trailing slash) is not mapped → 404", async () => {
+ const seen: string[] = [];
+ const resp = await worker.fetch(
+ new Request("https://docs.vyos.io/en/rolling/cli"),
+ makeAssetsEnv("production", seen),
+ );
+ expect(resp.status).toBe(404);
+ // passed through unmodified; html_handling:"none" does not infer ".html"
+ expect(seen).toEqual(["https://docs.vyos.io/en/rolling/cli"]);
+ // a 404 must never carry a cacheable page/asset class
+ expect(resp.headers.get("Cache-Control")).toBe("no-store");
+ });
+
+ it("directory mapping preserves the original query string", async () => {
+ const seen: string[] = [];
+ const resp = await worker.fetch(
+ new Request("https://docs.vyos.io/en/rolling/?q=foo"),
+ makeAssetsEnv("production", seen),
+ );
+ expect(resp.status).toBe(200);
+ expect(seen).toEqual(["https://docs.vyos.io/en/rolling/index.html?q=foo"]);
+ });
+});
diff --git a/workers/branch/wrangler.legacy.jsonc b/workers/branch/wrangler.legacy.jsonc
index fd036ec5..aef5144e 100644
--- a/workers/branch/wrangler.legacy.jsonc
+++ b/workers/branch/wrangler.legacy.jsonc
@@ -8,7 +8,7 @@
"assets": {
"directory": "../../dist/assets", // populated by CI (Task 3.2); html_handling per §3.2.3
"binding": "ASSETS",
- "html_handling": "auto-trailing-slash",
+ "html_handling": "none", // "none" + worker index-mapping: RTD parity — explicit .html URLs must serve 200, never 307 (§3.2.3 amended 2026-07-22)
"not_found_handling": "404-page",
"run_worker_first": true
},
diff --git a/workers/branch/wrangler.rolling.jsonc b/workers/branch/wrangler.rolling.jsonc
index 4595a06a..7a6f646c 100644
--- a/workers/branch/wrangler.rolling.jsonc
+++ b/workers/branch/wrangler.rolling.jsonc
@@ -8,7 +8,7 @@
"assets": {
"directory": "../../dist/assets", // populated by CI (Task 3.2); html_handling per §3.2.3
"binding": "ASSETS",
- "html_handling": "auto-trailing-slash",
+ "html_handling": "none", // "none" + worker index-mapping: RTD parity — explicit .html URLs must serve 200, never 307 (§3.2.3 amended 2026-07-22)
"not_found_handling": "404-page",
"run_worker_first": true
},
diff --git a/workers/branch/wrangler.v14.jsonc b/workers/branch/wrangler.v14.jsonc
index 2c7310aa..fe758dd9 100644
--- a/workers/branch/wrangler.v14.jsonc
+++ b/workers/branch/wrangler.v14.jsonc
@@ -8,7 +8,7 @@
"assets": {
"directory": "../../dist/assets", // populated by CI (Task 3.2); html_handling per §3.2.3
"binding": "ASSETS",
- "html_handling": "auto-trailing-slash",
+ "html_handling": "none", // "none" + worker index-mapping: RTD parity — explicit .html URLs must serve 200, never 307 (§3.2.3 amended 2026-07-22)
"not_found_handling": "404-page",
"run_worker_first": true
},
diff --git a/workers/branch/wrangler.v15.jsonc b/workers/branch/wrangler.v15.jsonc
index 45098aee..f2a0d7a4 100644
--- a/workers/branch/wrangler.v15.jsonc
+++ b/workers/branch/wrangler.v15.jsonc
@@ -8,7 +8,7 @@
"assets": {
"directory": "../../dist/assets", // populated by CI (Task 3.2); html_handling per §3.2.3
"binding": "ASSETS",
- "html_handling": "auto-trailing-slash",
+ "html_handling": "none", // "none" + worker index-mapping: RTD parity — explicit .html URLs must serve 200, never 307 (§3.2.3 amended 2026-07-22)
"not_found_handling": "404-page",
"run_worker_first": true
},