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
- Introduction
- Understanding the Challenge
- The Solution Overview
- Detailed Explanation of the Worker Script
- Implementing the Worker
- Security and Privacy Considerations
- Testing and Deployment
- Conclusion
- Full Worker Script
Introduction
This article demonstrates how to create a Cloudflare Worker script that:
- Implements geo-specific caching based on the user’s country and region.
- Caches responses to POST requests safely.
- Bypasses the cache for logged-in users to ensure personalized content is delivered correctly.
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:
- Creating a unique cache key that accounts for the request method, URL, body, and user location.
- Handling logged-in users by bypassing the cache to deliver personalized content.
- Ensuring security and privacy by not caching sensitive information or exposing user-specific data.
The Solution Overview
The provided Cloudflare Worker script addresses these challenges by:
- Generating a unique cache key that includes the country, region code, request method, and a hash of the request body for POST requests.
- Modifying the cache behavior to cache responses to POST requests safely.
- Adding custom headers to include geographical information in the response.
- Bypassing the cache for logged-in users to ensure they receive real-time, personalized content.
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:
- Country and Region Code: The cache key includes the user’s country and, if applicable, the region code (specifically for Canada).
- Request Body Hash: For POST requests, the body is hashed using SHA-256, and the hash is included in the cache key. This ensures that different request bodies result in different cache keys.
- Request Method: The request method is added to differentiate between GET and POST requests.
- Method Adjustment for Cache API: The cache key request method is set to
GET
because the Cache API only supports GET requests for storage.
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:
- Country and Region Retrieval: The script obtains the country and region code from the request headers.
- Logged-in User Detection: If a WordPress logged-in cookie is detected, the cache is bypassed to serve fresh content.
- Cache Check and Fetch: The script checks if the response exists in the cache. If not, it fetches from the origin server.
- Response Modification: Before caching, sensitive headers like
Set-Cookie
are removed to prevent caching user-specific data. - Custom Headers: The
cf-region
andcf-regioncode
headers are added to the response for additional context.
Caching the Response
- Cache-Control Headers: The script sets
Cache-Control: public, max-age=3600
to allow caching for one hour. - Event Wait Until:
event.waitUntil(cache.put(cacheKey, responseToCache))
ensures that the response is cached without delaying the response to the user.
Implementing the Worker
To implement this worker in your Cloudflare account:
-
Create a New Worker:
- Log in to your Cloudflare dashboard.
- Navigate to Workers and click Create a Worker.
-
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).
-
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.
- Set up the routes where the worker should be applied. For example,
-
Test the Worker:
- Use the built-in preview feature to test the worker.
- Make test requests to ensure caching behaves as expected.
-
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:
- Sensitive Data: Ensure that responses do not contain user-specific or sensitive information that could be exposed to other users.
- Logged-in Users: Bypass the cache for authenticated users to prevent serving personalized content from the cache.
- Headers: Remove or sanitize headers that could carry sensitive data, such as
Set-Cookie
. - Compliance: Be aware of regulations like GDPR and ensure that caching strategies comply with data protection laws.
Testing and Deployment
-
Cache Verification:
- Test cache hits and misses by making identical requests and checking the
cf-cache-status
header. - Use different locations (countries/regions) to verify geo-specific caching.
- Test cache hits and misses by making identical requests and checking the
-
Logged-in vs. Logged-out Users:
- Ensure that logged-in users receive fresh content and that the cache is bypassed.
-
Monitoring:
- Monitor cache performance using Cloudflare analytics.
- Use logging within the worker script (e.g.,
console.log
) for debugging purposes.
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.