Image Resizing through Workers not being put into Cache Reserv

Any idea why Image Resizing requests done through a Worker that listens on example.com/?image=path/to/image&w=__ for example, does not get stored into Cache Reserve? Overall, I’m getting cache hits upon repeat requests [using Cache API], so I know the image data is cacheable by query string. However, it’s ultimately not getting put into Cache Reserve, even with the Cache-Control headers to public, max-age=604800, stale-while-revalidate=86400 .

Is this an Images/Image Resizing integration issue with Cache Reserve somehow? TTFB on an uncached hit like anywhere from 1.5s–2.5s [kinda abysmal on 20 thumbs] but only like 100ms on a cached hit.

Code for the Worker handling Image Resizing:

addEventListener("fetch", event => {
  event.respondWith(handleRequest(event))
})

async function handleRequest(event) {
  const request = event.request
  if(request.method == "GET") {
      // Hook in to the cache and have any cache-control headers respected
      const cache = caches.default;
      let resp = await cache.match(request.url + "?" + format)

      if (!resp) {
          resp = await runResize(request)
          if(resp.status == 200) {
              event.waitUntil(cache.put(request.url + "?" + format, resp.clone()))
          }
      } else {
        console.log("Matched")
      }
      return resp
  }

  const resp = await runResize(request)
  return resp
}

// Image Resizing Handler
async function runResize(request) {
  // Parse request URL to get access to query string
  let url = new URL(request.url)

  // Cloudflare-specific options are in the cf object.
  let options = { cf: { image: {} } }

  // Copy parameters from query string to request options.
  // You can implement various different parameters here.
  options.cf.image.fit = "contain"
  if (url.searchParams.has("w")) options.cf.image.width = url.searchParams.get("w")
  if (url.searchParams.has("q")) {
      options.cf.image.quality = url.searchParams.get("q") 
  }

  const accept = request.headers.get("Accept");
  if (/image\/avif/.test(accept)) {
    options.cf.image.format = 'avif';
  } else if (/image\/webp/.test(accept)) {
    options.cf.image.format = 'webp';
  }

  const imageURL = // URL for R2 Worker Endpoint here

  // Build a request that passes through request headers
  const imageRequest = new Request(imageURL, {
    headers: request.headers
  })
  return fetch(imageRequest, options)
}

Potentially of note, the “origin” of the images is from a Cloudflare R2 private bucket that is exposed through a URL using a trimmed down version of kotx/render:

interface Env {
  R2_BUCKET: R2Bucket,
  CACHE_CONTROL?: string,
  PATH_PREFIX?: string,
  URL_ENDPOINT?: string
}

type ParsedRange = { offset: number, length: number } | { suffix: number };

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const allowedMethods = ["GET", "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(", ") } })
    }

    const url = new URL(request.url);
    if (url.pathname === "/") {
      return new Response("OK");
    }

    const cache = caches.default;
    let response = await cache.match(request);

    let range: ParsedRange | undefined;

    if (!response || !response.ok) {

      const endpointLen = ; // Needed to extract path in next step
      
      const path = ; // Extract file path from request URL here;

      let file: R2Object | R2ObjectBody | null | 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 }));

      if (file === null) {
        return new Response("File Not Found", { status: 404 });
      }

      response = new Response((hasBody(file) && file.size !== 0) ? file.body : null, {
        status: range ? 206 : 200,
        headers: {
          // Headers tagging, see kotx/render for full code
        }
      });

      if (request.method === "GET" && !range)
        ctx.waitUntil(cache.put(request, response.clone()));
    }

    return response;
  },
};

Here are two Dr. Flare screenshots showing an uncached and a cached request [600ms TTFB → 60ms TTFB] of the same image:


This topic was automatically closed 15 days after the last reply. New replies are no longer allowed.