Worker caching analytics

I’m building a worker based on this article about using workers to cache GraphQL posts.

Everything is working in my preview and I’m not getting errors when I deploy it, but based on the workers analytics, it doesn’t appear to be caching.

Here’s what my worker looks like

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

async function handleRequest(event) {
  let request = event.request;
  let response;

  const body = await request.clone().text();
  const bodyJSON = body ? JSON.parse(body) : {};

  console.log(request.method);

  if (request.method !== 'POST' || !bodyJSON.query) {
    console.log('Return origin response');
    response = await fetch(request);
    return response;
  }

  const hash = await sha256(body);

  let url = new URL(request.url);
  url.pathname = "/posts" + url.pathname + hash;

  console.log('Try to load from cache', url.pathname);
  // Convert to a GET to be able to cache
  const cacheKey = new Request(url, {
    headers: request.headers,
    method: 'GET'
  });

  let cache = caches.default;
  // try to find the cache key in the cache
  response = await cache.match(cacheKey);

  // otherwise, fetch from origin
  if (!response) {
    // makes POST to the origin
    console.log('Load and cache');
    response = await fetch(request);
    event.waitUntil(cache.put(cacheKey, response.clone()));
  }
  return response;
}

async function sha256(message) {
  // encode as UTF-8
  const msgBuffer = new TextEncoder().encode(message);

  // hash the message
  const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);

  // convert ArrayBuffer to Array
  const hashArray = Array.from(new Uint8Array(hashBuffer));

  // convert bytes to hex string
  const hashHex = hashArray.map(b => ('00' + b.toString(16)).slice(-2)).join('');
  return hashHex;
}

My origin cache headers are set to 300s.

My workers per zone stats on the analytics tab look like this.

Total requests
Last 24 hours
287,546

Cached requests
Last 24 hours
2,277

Uncached requests
Last 24 hours
285,269

If I’m returning from the cache, shouldn’t these values be very different?

I also just discovered a second issue. The worker returns a CORS violation when I try to upload a file. Other POSTS/GETS work as expected. When I remove the worker from my route, file uploads work again.

Is there something special I need to do to get form data to just flow to the origin server?

For anyone who comes to this, apparently 500 errors within a worker will throw a CORS error, which was misleading to me.

I ended confirming cache hits working by setting a header and by following along with the wordpress edge caching example, but it still has a pretty low cache count in the worker dashboard. I’m wondering if the response is getting cached for each user’s session.

Does anyone know if there are any headers that I should delete from the request/response headers? I’m currently deleting cache-control from the request and set-cookie from the response.

Here’s my current worker

async function sha256(message) {
  // encode as UTF-8
  const msgBuffer = new TextEncoder().encode(message)
  // hash the message
  const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer)
  // convert ArrayBuffer to Array
  const hashArray = Array.from(new Uint8Array(hashBuffer))
  // convert bytes to hex string
  const hashHex = hashArray.map(b => ('00' + b.toString(16)).slice(-2)).join('')
  return hashHex
}

function responseWithHeaders(resp, cached, error, post) {
  const response = new Response(resp.body, resp)
  response.headers.set('x-cached', cached)
  response.headers.set('x-error', error)
  response.headers.set('x-post', post)
  return response;
}

// process GETs from the cache
async function processRequest(originalRequest, event) {
  let request = new Request(originalRequest)
  let response
  let cache = caches.default
  let cached = true

  // try to find the cache key in the cache
  response = await cache.match(originalRequest)

  if (!response) {
    cached = false
    response = await fetch(originalRequest)
    event.waitUntil(cache.put(originalRequest, response.clone()))
  }
  return responseWithHeaders(response, cached, false, false);
}

/**
 * Fetch and log a request
 * @param {Request} request
 */
async function processPostRequest(originalRequest, event) {
  let request = new Request(originalRequest)
  const body = await request.clone().text()
  let hash = await sha256(body)
  let bodyJSON = {}
  let error = false

  try {
    bodyJSON = body ? JSON.parse(body) : {}
  } catch (ex) {
    error = ex.message
  }

  // If it's a mutation, or there was an error parsing the json pass the request through.
  if (!bodyJSON.query || error) {
    const originResponse = await fetch(request)
    return responseWithHeaders(originResponse, false, error, false)
  }

  let cacheUrl = new URL(request.url)
  // get/store the URL in cache by prepending the body's hash
  cacheUrl.pathname = `/posts${cacheUrl.pathname}${hash}`

  // Convert to a GET to be able to cache
  let cacheKeyRequest = new Request(cacheUrl, {
    headers: request.headers,
    method: 'GET',
  })
  cacheKeyRequest.headers.delete('Set-Cookie')

  let { response, status } = await getCachedResponse(originalRequest, cacheKeyRequest)

  if (response === null) {
    response = await fetch(request)

    if (response) {
      status += await cacheResponse(originalRequest, cacheKeyRequest, response, event)
    }
  }

  if (response && status !== null && response.status === 200) {
    response = new Response(response.body, response)
    response.headers.set('x-cache-status', status)
  }

  return response
}

//try to find the cache key in the cache
async function getCachedResponse(request, cacheKeyRequest) {
  let response = null
  let status = 'Miss'
  let cache = caches.default
  let cachedResponse = await cache.match(cacheKeyRequest)

  if (cachedResponse) {
    // Copy Response object so that we can edit headers.
    cachedResponse = new Response(cachedResponse.body, cachedResponse)
    status = 'Hit'
  }
  return { response, status };
}

async function cacheResponse(request, cacheKeyRequest, originalResponse, event) {
  let status = ''
  let cache = caches.default
  let clonedResponse = originalResponse.clone()
  let response = new Response(clonedResponse.body, clonedResponse)
  response.headers.delete('Set-Cookie')
  response.headers.set('Cache-Control', 'public; max-age=300')
  event.waitUntil(cache.put(cacheKeyRequest, response))
  status = ', Cached'
  return status
}

addEventListener('fetch', event => {
  event.passThroughOnException()
  let request = event.request
  if (request.method.toUpperCase() === 'POST') {
    return event.respondWith(processPostRequest(request, event))
  }
  return event.respondWith(processRequest(request, event))
})

Request headers:

:authority: example.com
:method: POST
:path: /graphql
:scheme: https
accept: */*
accept-encoding: gzip, deflate, br
accept-language: en-US,en;q=0.9
cache-control: no-cache
content-length: 463
content-type: application/json
origin: https://www.example.com
pragma: no-cache
referer: https://www.example.com/collections/new-arrivals
sec-fetch-mode: cors
sec-fetch-site: cross-site
user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.16 Safari/537.36

Response headers:

access-control-allow-origin: *
cache-control: max-age=300, public
cf-cache-status: DYNAMIC
cf-ray: 530baaf8ffad7b0a-MCI
content-encoding: gzip
content-type: application/json
date: Tue, 05 Nov 2019 03:16:14 GMT
expect-ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"
server: cloudflare
set-cookie: foobar
status: 200
strict-transport-security: max-age=15724800; includeSubDomains
vary: Accept-Encoding
vary: Cookie, Origin
x-cache-status: Hit, Cached
x-frame-options: SAMEORIGIN