summaryrefslogtreecommitdiff
path: root/workers/preview/src/index.ts
blob: 5c945175bff198f2b0e2a69ca046a117e0d181d9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
export interface Env { PREVIEWS: R2Bucket }

const MIME: Record<string, string> = {
  html: "text/html; charset=utf-8", css: "text/css", js: "text/javascript",
  json: "application/json", svg: "image/svg+xml", png: "image/png", jpg: "image/jpeg",
  gif: "image/gif", ico: "image/x-icon", txt: "text/plain; charset=utf-8",
  xml: "application/xml", pdf: "application/pdf", woff2: "font/woff2", woff: "font/woff",
};

export function mimeFor(key: string): string {
  const ext = key.split(".").pop() ?? "";
  return MIME[ext] ?? "application/octet-stream";
}

export function keyFor(pathname: string): string {
  let key = pathname.replace(/^\//, "");
  if (key.endsWith("/") || key === "" ) key += "index.html";
  return key;
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const key = keyFor(new URL(request.url).pathname);
    let obj: R2ObjectBody | null = null;
    let resolvedKey = key;
    try {
      obj = await env.PREVIEWS.get(key);
      if (!obj && !key.split("/").pop()?.includes(".")) {
        // Extensionless directory URL with no trailing slash (e.g. /en/rolling/cli) —
        // keyFor() only appends index.html for trailing-slash/empty paths, so probe the
        // directory's index.html before 404ing. Check the LAST path segment only — a dot
        // anywhere earlier (e.g. version segment "1.4" in /pr-42/en/1.4/cli) must not skip
        // the probe for an otherwise-extensionless final segment.
        resolvedKey = `${key}/index.html`;
        obj = await env.PREVIEWS.get(resolvedKey);
      }
    } catch {
      // Transient R2/binding error on either probe — fail closed with a controlled 503
      // instead of letting an unhandled exception surface as a raw worker error.
      return new Response("preview temporarily unavailable", {
        status: 503,
        headers: { "X-Robots-Tag": "noindex", "Cache-Control": "no-store" },
      });
    }
    if (!obj) {
      return new Response("preview not found", {
        status: 404,
        // no-store on the 404 too — a cached 404 would persist past the preview upload
        headers: { "X-Robots-Tag": "noindex", "Cache-Control": "no-store" },
      });
    }
    return new Response(obj.body, {
      headers: {
        "content-type": obj.httpMetadata?.contentType ?? mimeFor(resolvedKey),
        "X-Robots-Tag": "noindex",
        "Cache-Control": "no-store",
        "X-Content-Type-Options": "nosniff",
      },
    });
  },
} satisfies ExportedHandler<Env>;