branch:
asset-handler.ts
27194 bytesRaw
/**
 * Asset request handler for serving static assets.
 *
 * Key design: the manifest (routing metadata) is separated from the
 * storage (content retrieval). This lets you plug in any backend —
 * in-memory, KV, R2, Workspace, etc.
 *
 * Inspired by Cloudflare's Workers Static Assets behavior and
 * cloudflare-asset-worker by Timo Wilhelm.
 */

import { inferContentType } from "./mime";

// ── Storage interface ───────────────────────────────────────────────

/**
 * Pluggable storage backend for asset content.
 * Implement this to serve assets from KV, R2, Workspace, or any other source.
 */
export interface AssetStorage {
  get(pathname: string): Promise<ReadableStream | ArrayBuffer | string | null>;
}

/**
 * Metadata for a single asset (no content — that comes from storage).
 */
export interface AssetMetadata {
  contentType: string | undefined;
  etag: string;
}

/**
 * The manifest maps pathnames to metadata. Used for routing decisions,
 * ETag checks, and content-type headers — all without touching storage.
 */
export type AssetManifest = Map<string, AssetMetadata>;

/**
 * Create an in-memory storage backend from a pathname->content map.
 * This is the zero-config default for small asset sets.
 */
export function createMemoryStorage(
  assets: Record<string, string | ArrayBuffer>
): AssetStorage {
  const map = new Map(Object.entries(assets));
  return {
    get(pathname) {
      return Promise.resolve(map.get(pathname) ?? null);
    }
  };
}

// ── Configuration ───────────────────────────────────────────────────

/**
 * Configuration for asset serving behavior.
 */
export interface AssetConfig {
  /**
   * How to handle HTML file resolution and trailing slashes.
   * @default 'auto-trailing-slash'
   */
  html_handling?:
    | "auto-trailing-slash"
    | "force-trailing-slash"
    | "drop-trailing-slash"
    | "none";

  /**
   * How to handle requests that don't match any asset.
   * - 'single-page-application': Serve /index.html for 404s
   * - '404-page': Serve nearest 404.html walking up the directory tree
   * - 'none': Return null (fall through)
   * @default 'none'
   */
  not_found_handling?: "single-page-application" | "404-page" | "none";

  /**
   * Static redirect rules. Keys are URL pathnames (or https://host/path for cross-host).
   * Supports * glob and :placeholder tokens.
   */
  redirects?: {
    static?: Record<string, { status: number; to: string }>;
    dynamic?: Record<string, { status: number; to: string }>;
  };

  /**
   * Custom response headers per pathname pattern (glob syntax).
   */
  headers?: Record<string, { set?: Record<string, string>; unset?: string[] }>;
}

/**
 * Normalized configuration with all fields required.
 */
interface NormalizedConfig {
  html_handling:
    | "auto-trailing-slash"
    | "force-trailing-slash"
    | "drop-trailing-slash"
    | "none";
  not_found_handling: "single-page-application" | "404-page" | "none";
  redirects: {
    static: Record<string, { status: number; to: string; lineNumber: number }>;
    dynamic: Record<string, { status: number; to: string }>;
  };
  headers: Record<string, { set?: Record<string, string>; unset?: string[] }>;
}

/**
 * Normalize user config with defaults.
 */
export function normalizeConfig(config?: AssetConfig): NormalizedConfig {
  const staticRedirects: Record<
    string,
    { status: number; to: string; lineNumber: number }
  > = {};
  if (config?.redirects?.static) {
    let lineNumber = 1;
    for (const [path, rule] of Object.entries(config.redirects.static)) {
      staticRedirects[path] = { ...rule, lineNumber: lineNumber++ };
    }
  }

  return {
    html_handling: config?.html_handling ?? "auto-trailing-slash",
    not_found_handling: config?.not_found_handling ?? "none",
    redirects: {
      static: staticRedirects,
      dynamic: config?.redirects?.dynamic ?? {}
    },
    headers: config?.headers ?? {}
  };
}

// ── ETag / manifest building ────────────────────────────────────────

/**
 * Compute a simple hash for ETag generation.
 * Uses a fast string hash (FNV-1a) for text, or SHA-256 for binary.
 */
export async function computeETag(
  content: string | ArrayBuffer
): Promise<string> {
  if (typeof content === "string") {
    // FNV-1a hash for fast text hashing
    let hash = 2166136261;
    for (let i = 0; i < content.length; i++) {
      hash ^= content.charCodeAt(i);
      hash = (hash * 16777619) >>> 0;
    }
    return hash.toString(16).padStart(8, "0");
  }
  // SHA-256 for binary
  const hashBuffer = await crypto.subtle.digest("SHA-256", content);
  const hashArray = new Uint8Array(hashBuffer);
  return [...hashArray.slice(0, 8)]
    .map((b) => b.toString(16).padStart(2, "0"))
    .join("");
}

/**
 * Build an AssetManifest from a pathname->content mapping.
 * Only computes metadata (content types, ETags) — doesn't store content.
 */
export async function buildAssetManifest(
  assets: Record<string, string | ArrayBuffer>
): Promise<AssetManifest> {
  const manifest: AssetManifest = new Map();
  const entries = Object.entries(assets);
  await Promise.all(
    entries.map(async ([pathname, content]) => {
      const contentType = inferContentType(pathname);
      const etag = await computeETag(content);
      manifest.set(pathname, { contentType, etag });
    })
  );
  return manifest;
}

/**
 * Convenience: build both a manifest and an in-memory storage from assets.
 */
export async function buildAssets(
  assets: Record<string, string | ArrayBuffer>
): Promise<{ manifest: AssetManifest; storage: AssetStorage }> {
  const manifest = await buildAssetManifest(assets);
  const storage = createMemoryStorage(assets);
  return { manifest, storage };
}

/**
 * Check if a pathname exists in the manifest.
 */
function exists(
  manifest: AssetManifest,
  pathname: string
): AssetMetadata | undefined {
  return manifest.get(pathname);
}

// ── Redirect handling ───────────────────────────────────────────────

const ESCAPE_REGEX_CHARACTERS = /[-/\\^$*+?.()|[\]{}]/g;
const escapeRegex = (s: string) =>
  s.replaceAll(ESCAPE_REGEX_CHARACTERS, String.raw`\$&`);

const PLACEHOLDER_REGEX = /:([A-Za-z]\w*)/g;

type Replacements = Record<string, string>;

function replacer(str: string, replacements: Replacements): string {
  for (const [key, value] of Object.entries(replacements)) {
    str = str.replaceAll(`:${key}`, value);
  }
  return str;
}

function generateRuleRegExp(rule: string): RegExp {
  rule = rule
    .split("*")
    .map((s) => escapeRegex(s))
    .join("(?<splat>.*)");

  const matches = rule.matchAll(PLACEHOLDER_REGEX);
  for (const match of matches) {
    rule = rule.split(match[0]).join(`(?<${match[1]}>[^/]+)`);
  }

  return new RegExp("^" + rule + "$");
}

function matchStaticRedirects(
  config: NormalizedConfig,
  host: string,
  pathname: string
): { status: number; to: string; lineNumber: number } | undefined {
  const withHost = config.redirects.static[`https://${host}${pathname}`];
  const withoutHost = config.redirects.static[pathname];
  if (withHost && withoutHost) {
    return withHost.lineNumber < withoutHost.lineNumber
      ? withHost
      : withoutHost;
  }
  return withHost || withoutHost;
}

function matchDynamicRedirects(
  config: NormalizedConfig,
  request: Request
): { status: number; to: string } | undefined {
  const { pathname } = new URL(request.url);
  for (const [pattern, rule] of Object.entries(config.redirects.dynamic)) {
    try {
      const re = generateRuleRegExp(pattern);
      const result = re.exec(pathname);
      if (result) {
        const target = replacer(rule.to, result.groups || {}).trim();
        return { status: rule.status, to: target };
      }
    } catch {
      // Skip invalid patterns
    }
  }
  return undefined;
}

function handleRedirects(
  request: Request,
  config: NormalizedConfig
): Response | { proxied: boolean; pathname: string } {
  const url = new URL(request.url);
  const { search, host } = url;
  let { pathname } = url;

  const staticMatch = matchStaticRedirects(config, host, pathname);
  const dynamicMatch = staticMatch
    ? undefined
    : matchDynamicRedirects(config, request);
  const match = staticMatch ?? dynamicMatch;

  let proxied = false;
  if (match) {
    if (match.status === 200) {
      pathname = new URL(match.to, request.url).pathname;
      proxied = true;
    } else {
      const destination = new URL(match.to, request.url);
      const location =
        destination.origin === url.origin
          ? `${destination.pathname}${destination.search || search}${destination.hash}`
          : `${destination.href}`;
      return new Response(null, {
        status: match.status,
        headers: { Location: location }
      });
    }
  }

  return { proxied, pathname };
}

// ── Custom headers ──────────────────────────────────────────────────

function generateGlobRegExp(pattern: string): RegExp {
  const escaped = pattern
    .split("*")
    .map((s) => escapeRegex(s))
    .join(".*");
  return new RegExp("^" + escaped + "$");
}

function attachCustomHeaders(
  request: Request,
  response: Response,
  config: NormalizedConfig
): Response {
  if (Object.keys(config.headers).length === 0) {
    return response;
  }

  const { pathname } = new URL(request.url);
  const setMap = new Set<string>();

  for (const [pattern, rules] of Object.entries(config.headers)) {
    try {
      const re = generateGlobRegExp(pattern);
      if (!re.test(pathname)) continue;
    } catch {
      continue;
    }

    if (rules.unset) {
      for (const key of rules.unset) {
        response.headers.delete(key);
      }
    }
    if (rules.set) {
      for (const [key, value] of Object.entries(rules.set)) {
        if (setMap.has(key.toLowerCase())) {
          response.headers.append(key, value);
        } else {
          response.headers.set(key, value);
          setMap.add(key.toLowerCase());
        }
      }
    }
  }

  return response;
}

// ── Path decoding / encoding ────────────────────────────────────────

function decodePath(pathname: string): string {
  return pathname
    .split("/")
    .map((segment) => {
      try {
        return decodeURIComponent(segment);
      } catch {
        return segment;
      }
    })
    .join("/")
    .replaceAll(/\/+/g, "/");
}

function encodePath(pathname: string): string {
  return pathname
    .split("/")
    .map((segment) => {
      try {
        return encodeURIComponent(segment);
      } catch {
        return segment;
      }
    })
    .join("/");
}

// ── HTML handling modes ─────────────────────────────────────────────

type Intent =
  | { type: "asset"; pathname: string; meta: AssetMetadata; status: number }
  | { type: "redirect"; to: string }
  | undefined;

function getIntent(
  pathname: string,
  manifest: AssetManifest,
  config: NormalizedConfig,
  skipRedirects = false,
  acceptsHtml = true
): Intent {
  switch (config.html_handling) {
    case "auto-trailing-slash":
      return htmlAutoTrailingSlash(
        pathname,
        manifest,
        config,
        skipRedirects,
        acceptsHtml
      );
    case "force-trailing-slash":
      return htmlForceTrailingSlash(
        pathname,
        manifest,
        config,
        skipRedirects,
        acceptsHtml
      );
    case "drop-trailing-slash":
      return htmlDropTrailingSlash(
        pathname,
        manifest,
        config,
        skipRedirects,
        acceptsHtml
      );
    case "none":
      return htmlNone(pathname, manifest, config, acceptsHtml);
  }
}

function assetIntent(
  pathname: string,
  meta: AssetMetadata,
  status = 200
): Intent {
  return { type: "asset", pathname, meta, status };
}

function redirectIntent(to: string): Intent {
  return { type: "redirect", to };
}

/**
 * Safe redirect: only redirect if the file exists and the destination
 * itself resolves to the same asset (avoids redirect loops).
 */
function safeRedirect(
  file: string,
  destination: string,
  manifest: AssetManifest,
  config: NormalizedConfig,
  skip: boolean
): Intent {
  if (skip) return undefined;
  if (!exists(manifest, destination)) {
    const intent = getIntent(destination, manifest, config, true);
    if (
      intent?.type === "asset" &&
      intent.meta.etag === exists(manifest, file)?.etag
    ) {
      return redirectIntent(destination);
    }
  }
  return undefined;
}

function htmlAutoTrailingSlash(
  pathname: string,
  manifest: AssetManifest,
  config: NormalizedConfig,
  skipRedirects: boolean,
  acceptsHtml: boolean
): Intent {
  let meta: AssetMetadata | undefined;
  let redirect: Intent;
  const exactMeta = exists(manifest, pathname);

  if (pathname.endsWith("/index")) {
    if (exactMeta) {
      return assetIntent(pathname, exactMeta);
    }
    if (
      (redirect = safeRedirect(
        `${pathname}.html`,
        pathname.slice(0, -"index".length),
        manifest,
        config,
        skipRedirects
      ))
    )
      return redirect;
    if (
      (redirect = safeRedirect(
        `${pathname.slice(0, -"/index".length)}.html`,
        pathname.slice(0, -"/index".length),
        manifest,
        config,
        skipRedirects
      ))
    )
      return redirect;
  } else if (pathname.endsWith("/index.html")) {
    if (
      (redirect = safeRedirect(
        pathname,
        pathname.slice(0, -"index.html".length),
        manifest,
        config,
        skipRedirects
      ))
    )
      return redirect;
    if (
      (redirect = safeRedirect(
        `${pathname.slice(0, -"/index.html".length)}.html`,
        pathname.slice(0, -"/index.html".length),
        manifest,
        config,
        skipRedirects
      ))
    )
      return redirect;
  } else if (pathname.endsWith("/")) {
    const indexPath = `${pathname}index.html`;
    if ((meta = exists(manifest, indexPath))) {
      return assetIntent(indexPath, meta);
    }
    if (
      (redirect = safeRedirect(
        `${pathname.slice(0, -"/".length)}.html`,
        pathname.slice(0, -"/".length),
        manifest,
        config,
        skipRedirects
      ))
    )
      return redirect;
  } else if (pathname.endsWith(".html")) {
    if (
      (redirect = safeRedirect(
        pathname,
        pathname.slice(0, -".html".length),
        manifest,
        config,
        skipRedirects
      ))
    )
      return redirect;
    if (
      (redirect = safeRedirect(
        `${pathname.slice(0, -".html".length)}/index.html`,
        `${pathname.slice(0, -".html".length)}/`,
        manifest,
        config,
        skipRedirects
      ))
    )
      return redirect;
  }

  // Exact match
  if (exactMeta) {
    return assetIntent(pathname, exactMeta);
  }
  // Try .html extension
  const htmlPath = `${pathname}.html`;
  if ((meta = exists(manifest, htmlPath))) {
    return assetIntent(htmlPath, meta);
  }
  // Try /index.html
  if (
    (redirect = safeRedirect(
      `${pathname}/index.html`,
      `${pathname}/`,
      manifest,
      config,
      skipRedirects
    ))
  )
    return redirect;

  return notFound(pathname, manifest, config, acceptsHtml);
}

function htmlForceTrailingSlash(
  pathname: string,
  manifest: AssetManifest,
  config: NormalizedConfig,
  skipRedirects: boolean,
  acceptsHtml: boolean
): Intent {
  let meta: AssetMetadata | undefined;
  let redirect: Intent;
  const exactMeta = exists(manifest, pathname);

  if (pathname.endsWith("/index")) {
    if (exactMeta) return assetIntent(pathname, exactMeta);
    if (
      (redirect = safeRedirect(
        `${pathname}.html`,
        pathname.slice(0, -"index".length),
        manifest,
        config,
        skipRedirects
      ))
    )
      return redirect;
    if (
      (redirect = safeRedirect(
        `${pathname.slice(0, -"/index".length)}.html`,
        pathname.slice(0, -"index".length),
        manifest,
        config,
        skipRedirects
      ))
    )
      return redirect;
  } else if (pathname.endsWith("/index.html")) {
    if (
      (redirect = safeRedirect(
        pathname,
        pathname.slice(0, -"index.html".length),
        manifest,
        config,
        skipRedirects
      ))
    )
      return redirect;
    if (
      (redirect = safeRedirect(
        `${pathname.slice(0, -"/index.html".length)}.html`,
        pathname.slice(0, -"index.html".length),
        manifest,
        config,
        skipRedirects
      ))
    )
      return redirect;
  } else if (pathname.endsWith("/")) {
    let p = `${pathname}index.html`;
    if ((meta = exists(manifest, p))) {
      return assetIntent(p, meta);
    }
    p = `${pathname.slice(0, -"/".length)}.html`;
    if ((meta = exists(manifest, p))) {
      return assetIntent(p, meta);
    }
  } else if (pathname.endsWith(".html")) {
    if (
      (redirect = safeRedirect(
        pathname,
        `${pathname.slice(0, -".html".length)}/`,
        manifest,
        config,
        skipRedirects
      ))
    )
      return redirect;
    if (exactMeta) return assetIntent(pathname, exactMeta);
    if (
      (redirect = safeRedirect(
        `${pathname.slice(0, -".html".length)}/index.html`,
        `${pathname.slice(0, -".html".length)}/`,
        manifest,
        config,
        skipRedirects
      ))
    )
      return redirect;
  }

  if (exactMeta) return assetIntent(pathname, exactMeta);
  if (
    (redirect = safeRedirect(
      `${pathname}.html`,
      `${pathname}/`,
      manifest,
      config,
      skipRedirects
    ))
  )
    return redirect;
  if (
    (redirect = safeRedirect(
      `${pathname}/index.html`,
      `${pathname}/`,
      manifest,
      config,
      skipRedirects
    ))
  )
    return redirect;

  return notFound(pathname, manifest, config, acceptsHtml);
}

function htmlDropTrailingSlash(
  pathname: string,
  manifest: AssetManifest,
  config: NormalizedConfig,
  skipRedirects: boolean,
  acceptsHtml: boolean
): Intent {
  let meta: AssetMetadata | undefined;
  let redirect: Intent;
  const exactMeta = exists(manifest, pathname);

  if (pathname.endsWith("/index")) {
    if (exactMeta) return assetIntent(pathname, exactMeta);
    if (pathname === "/index") {
      if (
        (redirect = safeRedirect(
          "/index.html",
          "/",
          manifest,
          config,
          skipRedirects
        ))
      )
        return redirect;
    } else {
      if (
        (redirect = safeRedirect(
          `${pathname.slice(0, -"/index".length)}.html`,
          pathname.slice(0, -"/index".length),
          manifest,
          config,
          skipRedirects
        ))
      )
        return redirect;
      if (
        (redirect = safeRedirect(
          `${pathname}.html`,
          pathname.slice(0, -"/index".length),
          manifest,
          config,
          skipRedirects
        ))
      )
        return redirect;
    }
  } else if (pathname.endsWith("/index.html")) {
    if (pathname === "/index.html") {
      if (
        (redirect = safeRedirect(
          "/index.html",
          "/",
          manifest,
          config,
          skipRedirects
        ))
      )
        return redirect;
    } else {
      if (
        (redirect = safeRedirect(
          pathname,
          pathname.slice(0, -"/index.html".length),
          manifest,
          config,
          skipRedirects
        ))
      )
        return redirect;
      if (exactMeta) return assetIntent(pathname, exactMeta);
      if (
        (redirect = safeRedirect(
          `${pathname.slice(0, -"/index.html".length)}.html`,
          pathname.slice(0, -"/index.html".length),
          manifest,
          config,
          skipRedirects
        ))
      )
        return redirect;
    }
  } else if (pathname.endsWith("/")) {
    if (pathname === "/") {
      if ((meta = exists(manifest, "/index.html"))) {
        return assetIntent("/index.html", meta);
      }
    } else {
      if (
        (redirect = safeRedirect(
          `${pathname.slice(0, -"/".length)}.html`,
          pathname.slice(0, -"/".length),
          manifest,
          config,
          skipRedirects
        ))
      )
        return redirect;
      if (
        (redirect = safeRedirect(
          `${pathname.slice(0, -"/".length)}/index.html`,
          pathname.slice(0, -"/".length),
          manifest,
          config,
          skipRedirects
        ))
      )
        return redirect;
    }
  } else if (pathname.endsWith(".html")) {
    if (
      (redirect = safeRedirect(
        pathname,
        pathname.slice(0, -".html".length),
        manifest,
        config,
        skipRedirects
      ))
    )
      return redirect;
    if (
      (redirect = safeRedirect(
        `${pathname.slice(0, -".html".length)}/index.html`,
        pathname.slice(0, -".html".length),
        manifest,
        config,
        skipRedirects
      ))
    )
      return redirect;
  }

  if (exactMeta) return assetIntent(pathname, exactMeta);
  let p = `${pathname}.html`;
  if ((meta = exists(manifest, p))) {
    return assetIntent(p, meta);
  }
  p = `${pathname}/index.html`;
  if ((meta = exists(manifest, p))) {
    return assetIntent(p, meta);
  }

  return notFound(pathname, manifest, config, acceptsHtml);
}

function htmlNone(
  pathname: string,
  manifest: AssetManifest,
  config: NormalizedConfig,
  acceptsHtml: boolean
): Intent {
  const meta = exists(manifest, pathname);
  return meta
    ? assetIntent(pathname, meta)
    : notFound(pathname, manifest, config, acceptsHtml);
}

// ── Not-found handling ──────────────────────────────────────────────

function notFound(
  pathname: string,
  manifest: AssetManifest,
  config: NormalizedConfig,
  acceptsHtml = true
): Intent {
  switch (config.not_found_handling) {
    case "single-page-application": {
      // Only serve the SPA fallback for requests that accept HTML
      // (browser navigation). API calls (Accept: */* or application/json)
      // should fall through to the user's server code.
      if (!acceptsHtml) return undefined;
      const meta = exists(manifest, "/index.html");
      if (meta) return assetIntent("/index.html", meta, 200);
      return undefined;
    }
    case "404-page": {
      let cwd = pathname;
      while (cwd) {
        cwd = cwd.slice(0, cwd.lastIndexOf("/"));
        const p = `${cwd}/404.html`;
        const meta = exists(manifest, p);
        if (meta) return assetIntent(p, meta, 404);
      }
      return undefined;
    }
    default:
      return undefined;
  }
}

// ── Cache headers ───────────────────────────────────────────────────

const CACHE_CONTROL_REVALIDATE = "public, max-age=0, must-revalidate";
const CACHE_CONTROL_IMMUTABLE = "public, max-age=31536000, immutable";

function getCacheControl(pathname: string): string {
  // Hashed assets (contain content hash in filename) get long-lived caching
  // Common patterns: app.abc123.js, styles.abc123.css, image.abc123.png
  if (/\.[a-f0-9]{8,}\.\w+$/.test(pathname)) {
    return CACHE_CONTROL_IMMUTABLE;
  }
  return CACHE_CONTROL_REVALIDATE;
}

// ── Main handler ────────────────────────────────────────────────────

/**
 * Handle an asset request. Returns a Response if an asset matches,
 * or null if the request should fall through to the user's Worker.
 *
 * @param request - The incoming HTTP request
 * @param manifest - Asset manifest (pathname -> metadata)
 * @param storage - Storage backend for fetching content
 * @param config - Asset serving configuration
 */
export async function handleAssetRequest(
  request: Request,
  manifest: AssetManifest,
  storage: AssetStorage,
  config?: AssetConfig
): Promise<Response | null> {
  const normalized = normalizeConfig(config);

  // Only handle GET and HEAD
  const method = request.method.toUpperCase();
  if (!["GET", "HEAD"].includes(method)) {
    return null;
  }

  // Check redirects first
  const redirectResult = handleRedirects(request, normalized);
  if (redirectResult instanceof Response) {
    return attachCustomHeaders(request, redirectResult, normalized);
  }

  const { pathname } = redirectResult;
  const decodedPathname = decodePath(pathname);

  // SPA fallback should only apply to navigation requests that explicitly
  // accept HTML, not to API calls (fetch/XHR) which have Accept: */*
  const accept = request.headers.get("Accept") || "";
  const acceptsHtml = accept.includes("text/html");

  // Resolve intent through HTML handling
  const intent = getIntent(
    decodedPathname,
    manifest,
    normalized,
    false,
    acceptsHtml
  );

  if (!intent) {
    return null;
  }

  if (intent.type === "redirect") {
    const url = new URL(request.url);
    const encodedDest = encodePath(intent.to);
    const response = new Response(null, {
      status: 307,
      headers: { Location: encodedDest + url.search }
    });
    return attachCustomHeaders(request, response, normalized);
  }

  // Check if canonical path differs (needs redirect)
  const encodedPathname = encodePath(decodedPathname);
  if (encodedPathname !== pathname) {
    const url = new URL(request.url);
    const response = new Response(null, {
      status: 307,
      headers: { Location: encodedPathname + url.search }
    });
    return attachCustomHeaders(request, response, normalized);
  }

  // ETag conditional request
  const { pathname: assetPath, meta, status } = intent;
  const strongETag = `"${meta.etag}"`;
  const weakETag = `W/${strongETag}`;
  const ifNoneMatch = request.headers.get("If-None-Match") || "";
  const eTags = new Set(ifNoneMatch.split(",").map((t) => t.trim()));

  const headers = new Headers();
  headers.set("ETag", strongETag);
  if (meta.contentType) {
    headers.set("Content-Type", meta.contentType);
  }
  headers.set("Cache-Control", getCacheControl(decodedPathname));

  if (eTags.has(weakETag) || eTags.has(strongETag)) {
    const response = new Response(null, { status: 304, headers });
    return attachCustomHeaders(request, response, normalized);
  }

  // Fetch content from storage (only for non-HEAD)
  let body: ReadableStream | ArrayBuffer | string | null = null;
  if (method !== "HEAD") {
    body = await storage.get(assetPath);
  }

  const response = new Response(body, { status, headers });
  return attachCustomHeaders(request, response, normalized);
}