basyliq 🌿

Geo Caching with Cloudflare Workers Include Caching POST Requests

Serving geo-specific content efficiently requires intelligent caching strategies to minimize latency and reduce server load. This article explores how to implement geo-specific caching in Cloudflare Workers, including caching of POST requests—a non-default behavior—using a custom worker script.


Table of Contents

  1. Introduction
  2. Understanding the Challenge
  3. The Solution Overview
  4. Detailed Explanation of the Worker Script
  5. Implementing the Worker
  6. Security and Privacy Considerations
  7. Testing and Deployment
  8. Conclusion
  9. Full Worker Script

Introduction

This article demonstrates how to create a Cloudflare Worker script that:

Understanding the Challenge

By default, Cloudflare caches GET requests but does not cache POST requests. POST requests often involve changing server state or handling sensitive data, so caching them requires careful consideration. In applications where POST requests return the same content for users from the same country (and region), caching these responses can significantly improve performance.

The main challenges are:

The Solution Overview

The provided Cloudflare Worker script addresses these challenges by:

Detailed Explanation of the Worker Script

Let’s break down the worker script to understand how it achieves geo-specific caching and caches POST requests.

Event Listener

1addEventListener('fetch', event => {
2  event.respondWith(handleRequest(event));
3});

The script listens for fetch events and invokes the handleRequest function for each incoming request.

Generating a Unique Cache Key

 1async function generateCacheKey(request, country, regionCode) {
 2  const cacheUrl = new URL(request.url);
 3
 4  // Modify the pathname to include country and regionCode
 5  if (country === 'CA' && regionCode) {
 6    cacheUrl.pathname = `/cf-country-${country}-region-${regionCode}${cacheUrl.pathname}`;
 7  } else {
 8    cacheUrl.pathname = `/cf-country-${country}${cacheUrl.pathname}`;
 9  }
10
11  // For POST requests, include the request body in the cache key
12  if (request.method === 'POST') {
13    const requestClone = request.clone();
14    const body = await requestClone.text();
15
16    // Create a hash of the request body
17    const hashBuffer = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(body));
18    const hashArray = Array.from(new Uint8Array(hashBuffer));
19    const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
20
21    // Include the hash in the cache key
22    cacheUrl.searchParams.set('body_hash', hashHex);
23  }
24
25  // Include the request method in the cache key
26  cacheUrl.searchParams.set('request_method', request.method);
27
28  // Use the modified URL as the cache key
29  const cacheKey = new Request(cacheUrl.toString(), {
30    method: 'GET',
31    headers: request.headers,
32  });
33  return cacheKey;
34}

Key Points:

Handling the Request

  1async function handleRequest(event) {
  2  const request = event.request;
  3  const cache = caches.default;
  4
  5  // Get country and region code from headers
  6  let country = request.headers.get('cf-ipcountry') || 'US';
  7  if (!country || country === 'XX') {
  8    country = 'US';
  9  }
 10  const regionCode = request.cf.regionCode || '';
 11
 12  // Check for WordPress logged-in cookie
 13  const cookie = request.headers.get('Cookie') || '';
 14  const isLoggedIn = /wordpress_logged_in_[^=]*=/.test(cookie);
 15
 16  // Get cf-region for response headers
 17  const region = request.cf.region || '';
 18
 19  if (isLoggedIn) {
 20    // Bypass cache for logged-in users
 21    let headers = new Headers(request.headers);
 22    headers.append('cf-region', region);
 23    headers.append('cf-regioncode', regionCode);
 24    let response = await fetch(request, { headers });
 25
 26    // Create new headers
 27    const newHeaders = new Headers(response.headers);
 28    newHeaders.set('Cache-Control', 'private, no-store');
 29    newHeaders.set('cf-region', region);
 30    newHeaders.set('cf-regioncode', regionCode);
 31
 32    // Return the response with modified headers
 33    return new Response(response.body, {
 34      status: response.status,
 35      statusText: response.statusText,
 36      headers: newHeaders,
 37    });
 38  }
 39
 40  // Generate a unique cache key
 41  const cacheKey = await generateCacheKey(request, country, regionCode);
 42
 43  // Check if the response is in the cache
 44  let response = await cache.match(cacheKey);
 45
 46  if (response) {
 47    // If response is found in cache, add headers and return it
 48    const newHeaders = new Headers(response.headers);
 49    newHeaders.set('cf-region', region);
 50    newHeaders.set('cf-regioncode', regionCode);
 51
 52    return new Response(response.body, {
 53      status: response.status,
 54      statusText: response.statusText,
 55      headers: newHeaders,
 56    });
 57  } else {
 58    // If not in cache, fetch from origin
 59    let headers = new Headers(request.headers);
 60    headers.append('cf-region', region);
 61    headers.append('cf-regioncode', regionCode);
 62
 63    // Clone the request to read the body if needed
 64    const requestClone = request.clone();
 65    let response = await fetch(requestClone, { headers });
 66
 67    if (response.ok) {
 68      // Clone the response for caching
 69      let responseToCache = response.clone();
 70
 71      // Remove sensitive headers
 72      const responseToCacheHeaders = new Headers(responseToCache.headers);
 73      responseToCacheHeaders.delete('Set-Cookie');
 74      responseToCacheHeaders.delete('Set-Cookie2');
 75      responseToCacheHeaders.set('Cache-Control', 'public, max-age=3600');
 76
 77      // Create a new Response for caching with updated headers
 78      responseToCache = new Response(responseToCache.body, {
 79        status: responseToCache.status,
 80        statusText: responseToCache.statusText,
 81        headers: responseToCacheHeaders,
 82      });
 83
 84      // Cache the response with the modified cache key
 85      event.waitUntil(cache.put(cacheKey, responseToCache));
 86    }
 87
 88    // Add headers to the original response for the user
 89    const newHeaders = new Headers(response.headers);
 90    newHeaders.set('cf-region', region);
 91    newHeaders.set('cf-regioncode', regionCode);
 92
 93    // Set appropriate Cache-Control header
 94    newHeaders.set('Cache-Control', 'public, max-age=3600');
 95
 96    return new Response(response.body, {
 97      status: response.status,
 98      statusText: response.statusText,
 99      headers: newHeaders,
100    });
101  }
102}

Key Points:

Caching the Response

Implementing the Worker

To implement this worker in your Cloudflare account:

  1. Create a New Worker:

    • Log in to your Cloudflare dashboard.
    • Navigate to Workers and click Create a Worker.
  2. Copy the Worker Script:

    • Replace any existing code in the editor with the provided worker script (see the full script at the end of the article).
  3. Configure Routes:

    • Set up the routes where the worker should be applied. For example, *yourdomain.com/*.
    • Ensure that the worker is enabled for the routes handling the requests you want to cache.
  4. Test the Worker:

    • Use the built-in preview feature to test the worker.
    • Make test requests to ensure caching behaves as expected.
  5. Deploy the Worker:

    • Once testing is complete, deploy the worker to your production environment.

Security and Privacy Considerations

When caching POST requests and geo-specific content, it’s crucial to address security and privacy concerns:

Testing and Deployment

Conclusion

Implementing geo-specific caching with Cloudflare Workers allows for efficient content delivery tailored to the user’s location. By carefully crafting cache keys and handling POST requests, you can significantly improve performance for your users. Always keep security and privacy at the forefront when caching content, especially when deviating from default behaviors like caching POST requests.

Full Worker Script

Below is the complete worker script discussed in this article:

  1addEventListener('fetch', event => {
  2  event.respondWith(handleRequest(event));
  3});
  4
  5async function generateCacheKey(request, country, regionCode) {
  6  const cacheUrl = new URL(request.url);
  7
  8  // Modify the pathname to include country and regionCode
  9  if (country === 'CA' && regionCode) {
 10    cacheUrl.pathname = `/cf-country-${country}-region-${regionCode}${cacheUrl.pathname}`;
 11  } else {
 12    cacheUrl.pathname = `/cf-country-${country}${cacheUrl.pathname}`;
 13  }
 14
 15  // For POST requests, include the request body in the cache key
 16  if (request.method === 'POST') {
 17    const requestClone = request.clone();
 18    const body = await requestClone.text();
 19
 20    // Create a hash of the request body
 21    const hashBuffer = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(body));
 22    const hashArray = Array.from(new Uint8Array(hashBuffer));
 23    const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
 24
 25    // Include the hash in the cache key
 26    cacheUrl.searchParams.set('body_hash', hashHex);
 27  }
 28
 29  // Include the request method in the cache key
 30  cacheUrl.searchParams.set('request_method', request.method);
 31
 32  // Use the modified URL as the cache key
 33  const cacheKey = new Request(cacheUrl.toString(), {
 34    method: 'GET',
 35    headers: request.headers,
 36  });
 37  return cacheKey;
 38}
 39
 40async function handleRequest(event) {
 41  const request = event.request;
 42  const cache = caches.default;
 43
 44  // Get country and region code from headers
 45  let country = request.headers.get('cf-ipcountry') || 'US';
 46  if (!country || country === 'XX') {
 47    country = 'US';
 48  }
 49  const regionCode = request.cf.regionCode || '';
 50
 51  // Check for WordPress logged-in cookie
 52  const cookie = request.headers.get('Cookie') || '';
 53  const isLoggedIn = /wordpress_logged_in_[^=]*=/.test(cookie);
 54
 55  // Get cf-region for response headers
 56  const region = request.cf.region || '';
 57
 58  if (isLoggedIn) {
 59    // Bypass cache for logged-in users
 60    let headers = new Headers(request.headers);
 61    headers.append('cf-region', region);
 62    headers.append('cf-regioncode', regionCode);
 63    let response = await fetch(request, { headers });
 64
 65    // Create new headers
 66    const newHeaders = new Headers(response.headers);
 67    newHeaders.set('Cache-Control', 'private, no-store');
 68    newHeaders.set('cf-region', region);
 69    newHeaders.set('cf-regioncode', regionCode);
 70
 71    // Return the response with modified headers
 72    return new Response(response.body, {
 73      status: response.status,
 74      statusText: response.statusText,
 75      headers: newHeaders,
 76    });
 77  }
 78
 79  // Generate a unique cache key
 80  const cacheKey = await generateCacheKey(request, country, regionCode);
 81
 82  // Check if the response is in the cache
 83  let response = await cache.match(cacheKey);
 84
 85  if (response) {
 86    // If response is found in cache, add headers and return it
 87    const newHeaders = new Headers(response.headers);
 88    newHeaders.set('cf-region', region);
 89    newHeaders.set('cf-regioncode', regionCode);
 90
 91    return new Response(response.body, {
 92      status: response.status,
 93      statusText: response.statusText,
 94      headers: newHeaders,
 95    });
 96  } else {
 97    // If not in cache, fetch from origin
 98    let headers = new Headers(request.headers);
 99    headers.append('cf-region', region);
100    headers.append('cf-regioncode', regionCode);
101
102    // Clone the request to read the body if needed
103    const requestClone = request.clone();
104    let response = await fetch(requestClone, { headers });
105
106    if (response.ok) {
107      // Clone the response for caching
108      let responseToCache = response.clone();
109
110      // Remove sensitive headers
111      const responseToCacheHeaders = new Headers(responseToCache.headers);
112      responseToCacheHeaders.delete('Set-Cookie');
113      responseToCacheHeaders.delete('Set-Cookie2');
114      responseToCacheHeaders.set('Cache-Control', 'public, max-age=3600');
115
116      // Create a new Response for caching with updated headers
117      responseToCache = new Response(responseToCache.body, {
118        status: responseToCache.status,
119        statusText: responseToCache.statusText,
120        headers: responseToCacheHeaders,
121      });
122
123      // Cache the response with the modified cache key
124      event.waitUntil(cache.put(cacheKey, responseToCache));
125    }
126
127    // Add headers to the original response for the user
128    const newHeaders = new Headers(response.headers);
129    newHeaders.set('cf-region', region);
130    newHeaders.set('cf-regioncode', regionCode);
131
132    // Set appropriate Cache-Control header
133    newHeaders.set('Cache-Control', 'public, max-age=3600');
134
135    return new Response(response.body, {
136      status: response.status,
137      statusText: response.statusText,
138      headers: newHeaders,
139    });
140  }
141}

Implement this worker script to enhance your web application’s responsiveness and provide a better user experience tailored to your audience’s location.

#servers #cache #cloudflare #geo #post #workers