async fetch()

in cloudflare/src/main.ts [53:234]


  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const allowedMethods = ["GET", "HEAD", "OPTIONS"];
    if (allowedMethods.indexOf(request.method) === -1) return new Response("Method Not Allowed", { status: 405 });

    if (request.method === "OPTIONS") {
      return new Response(null, { headers: { "allow": allowedMethods.join(", ") } })
    }

    let triedIndex = false;

    const url = new URL(request.url);

    const healthCheck = isHealthCheck(env, url);
    if (healthCheck) {
      if (env.HEALTHCHECK_UA_REGEXP && !request.headers.get("user-agent")?.match(env.HEALTHCHECK_UA_REGEXP)) {
        return new Response("File Not Found", { status: 404 });
      }

      url.pathname = '/ping';
    }

    // Trim the first part of the request
    url.hostname = url.hostname.replace(/^[^.]+\./, '');

    let response: Response | undefined;

    if (DEBUG) {
      console.debug(`Request URL: ${url.toString()}`);
    }

    const shouldCache = env.CACHE_CONTROL !== "no-store" && !healthCheck;
    const cache = caches.default;
    const cacheKey = new Request(url.toString(), request);
    if (shouldCache) {
      response = await cache.match(cacheKey);
    }

    let range: ParsedRange | undefined;

    if (!response || !(response.ok || response.status == 304)) {
      if (DEBUG) {
        console.warn(`Cache MISS for ${url.toString()}`);
      }
      let path = (env.PATH_PREFIX || "") + decodeURIComponent(url.pathname);

      if (path.endsWith("/")) {
        if (env.INDEX_FILE && env.INDEX_FILE !== "") {
          path += env.INDEX_FILE;
          triedIndex = true;
        }
      }

      if (path !== "/" && path.startsWith("/")) {
        path = path.substring(1);
      }

      let file: R2Object | R2ObjectBody | null | undefined;

      // Range handling
      if (request.method === "GET") {
        const rangeHeader = request.headers.get("range");
        if (rangeHeader) {
          file = await env.R2_BUCKET.head(path);
          if (!file) return new Response("File Not Found", { status: 404 });
          const parsedRanges = parseRange(file.size, rangeHeader);
          // R2 only supports 1 range at the moment, reject if there is more than one
          if (parsedRanges !== -1 && parsedRanges !== -2 && parsedRanges.length === 1 && parsedRanges.type === "bytes") {
            let firstRange = parsedRanges[0];
            range = file.size === (firstRange.end + 1) ? { suffix: file.size - firstRange.start } : {
              offset: firstRange.start,
              length: firstRange.end - firstRange.start + 1
            };
          } else {
            return new Response("Range Not Satisfiable", { status: 416 });
          }
        }
      }

      // Etag/If-(Not)-Match handling
      // R2 requires that etag checks must not contain quotes, and the S3 spec only allows one etag
      // This silently ignores invalid or weak (W/) headers
      const getHeaderEtag = (header: string | null) => header?.trim().replace(/^['"]|['"]$/g, "");
      const ifMatch = getHeaderEtag(request.headers.get("if-match"));
      const ifNoneMatch = getHeaderEtag(request.headers.get("if-none-match"));

      const ifModifiedSince = Date.parse(request.headers.get("if-modified-since") || "");
      const ifUnmodifiedSince = Date.parse(request.headers.get("if-unmodified-since") || "");

      const ifRange = request.headers.get("if-range");
      if (range && ifRange && file) {
        const maybeDate = Date.parse(ifRange);

        if (isNaN(maybeDate) || new Date(maybeDate) > file.uploaded) {
          // httpEtag already has quotes, no need to use getHeaderEtag
          if (ifRange.startsWith("W/") || ifRange !== file.httpEtag) range = undefined;
        }
      }

      if (ifMatch || ifUnmodifiedSince) {
        file = await env.R2_BUCKET.get(path, {
          onlyIf: {
            etagMatches: ifMatch,
            uploadedBefore: ifUnmodifiedSince ? new Date(ifUnmodifiedSince) : undefined
          }, range
        });

        if (file && !hasBody(file)) {
          return new Response("Precondition Failed", { status: 412 });
        }
      }

      if (ifNoneMatch || ifModifiedSince) {
        // if-none-match overrides if-modified-since completely
        if (ifNoneMatch) {
          file = await env.R2_BUCKET.get(path, { onlyIf: { etagDoesNotMatch: ifNoneMatch }, range });
        } else if (ifModifiedSince) {
          file = await env.R2_BUCKET.get(path, { onlyIf: { uploadedAfter: new Date(ifModifiedSince) }, range });
        }
        if (file && !hasBody(file)) {
          return new Response(null, { status: 304 });
        }
      }

      file = request.method === "HEAD"
        ? await env.R2_BUCKET.head(path)
        : ((file && hasBody(file)) ? file : await env.R2_BUCKET.get(path, { range }));

      let notFound: boolean = false;

      if (file === null) {
        if (env.INDEX_FILE && triedIndex) {
          // remove the index file since it doesnt exist
          path = path.substring(0, path.length - env.INDEX_FILE.length)
        }

        if (env.NOTFOUND_FILE && env.NOTFOUND_FILE != "") {
          notFound = true;
          path = env.NOTFOUND_FILE;
          file = request.method === "HEAD"
            ? await env.R2_BUCKET.head(path)
            : await env.R2_BUCKET.get(path);
        }

        // if its still null, either 404 is disabled or that file wasn't found either
        // this isn't an else because then there would have to be two of them
        if (file == null) {
          return new Response("File Not Found", { status: 404 });
        }
      }

      response = new Response((hasBody(file) && file.size !== 0) ? file.body : null, {
        status: notFound ? 404 : (range ? 206 : 200),
        headers: {
          "accept-ranges": "bytes",
          "access-control-allow-origin": env.ALLOWED_ORIGINS || "",

          "etag": notFound ? "" : file.httpEtag,
          // if the 404 file has a custom cache control, we respect it
          "cache-control": file.httpMetadata?.cacheControl ?? (notFound || healthCheck ? "" : env.CACHE_CONTROL || ""),
          "expires": file.httpMetadata?.cacheExpiry?.toUTCString() ?? "",
          "last-modified": notFound ? "" : file.uploaded.toUTCString(),

          "content-encoding": file.httpMetadata?.contentEncoding ?? "",
          "content-type": file.httpMetadata?.contentType ?? "application/octet-stream",
          "content-language": file.httpMetadata?.contentLanguage ?? "",
          "content-disposition": file.httpMetadata?.contentDisposition ?? "",
          "content-range": (range && !notFound ? getRangeHeader(range, file.size) : ""),
          "content-length": (range && !notFound ? (rangeHasLength(range) ? range.length : range.suffix) : file.size).toString(),
          "cross-origin-resource-policy": "cross-origin",
        }
      });

      if (request.method === "GET" && !range && shouldCache && !notFound)
        ctx.waitUntil(cache.put(cacheKey, response.clone()));
    } else {
      if (DEBUG) {
        console.info(`Cache HIT for ${url.toString()}`);
      }
    }

    return response;
  },