feat: add Cloudflare CDN edge caching for cacheable endpoints#64
Conversation
Add cache control headers that instruct Cloudflare CDN to cache
responses at the edge when the lookup IP is explicit (in URL path
or query string), while ensuring responses using the requester's IP
are never cached.
Uses Cloudflare-CDN-Cache-Control header to control edge caching
separately from browser caching (Cache-Control: private, no-store).
Cacheable endpoints (IP in path or ?ip= query):
- /v1/ip/country/{IP}, /v1/ip/country/{IP}.json, /v1/ip/country/{IP}.js
- /v1/ip/country/full/{IP}
- /v1/ip/geo/{IP}.json, /v1/ip/geo/{IP}.js
- /v1/dns/ptr/{IP}, /v1/dns/ptr/{IP}.json, /v1/dns/ptr/{IP}.js
- Any endpoint with ?ip= query parameter
Non-cacheable endpoints (uses requester's IP):
- /v1/ip, /v1/ip.json, /v1/ip.js
- /v1/ip/country, /v1/ip/country.json, etc. without IP param
- /v1/ip/geo.json, /v1/ip/geo.js without IP param
- /v1/dns/ptr, /v1/dns/ptr.json, etc. without IP param
There was a problem hiding this comment.
Pull request overview
Adds Cloudflare edge caching controls for deterministic (explicit-IP) endpoints while ensuring browsers never cache responses, improving CDN hit rates for stable lookups without serving stale client-side data.
Changes:
- Introduces
geojs.utils.set_cache_headers(is_cacheable)to consistently setCache-ControlandCloudflare-CDN-Cache-Control. - Applies cacheability rules across
/v1/ip*and/v1/dns/ptr*Nginx Lua handlers (explicit IP => cacheable; requester IP => non-cacheable). - Adds new Test::Nginx suites to assert caching headers for IP/country/geo and DNS PTR endpoints.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
lib/geojs/utils.lua |
Adds shared helper to set browser vs Cloudflare edge cache headers. |
conf/v1/ip.conf |
Marks explicit-IP IP/country/geo routes cacheable and requester-IP routes non-cacheable. |
conf/v1/dns.conf |
Marks explicit-IP DNS PTR routes cacheable and requester-IP routes non-cacheable. |
t/v1/ip/06-caching.t |
Tests caching headers for IP/country/geo endpoints across path/query variants. |
t/v1/ip/07-dns-caching.t |
Tests caching headers for DNS PTR endpoints across path/query variants. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if args.ip then | ||
| utils.set_cache_headers(true) -- Cacheable: explicit IP in query | ||
| record = args.ip | ||
| else | ||
| utils.set_cache_headers(false) -- Not cacheable: uses requester's IP | ||
| record = ngx.var.remote_addr | ||
| end | ||
| local ptr = getptr(record) | ||
| local ptr = utils.get_ptr(record) | ||
| ngx.say(ptr) |
There was a problem hiding this comment.
For the PTR endpoints, set_cache_headers(true) is set before performing the DNS lookup. Since utils.get_ptr() can return nil or error strings on transient failures (resolver init/query errors), Cloudflare could cache those failure responses for 1 year. Consider only marking the response cacheable after a successful lookup (and/or set a non-cacheable/short TTL on errors, potentially with an appropriate HTTP status).
| local utils = require("geojs.utils") | ||
| local ngx_vars = ngx.var | ||
|
|
||
| -- Vars | ||
| local record | ||
| utils.set_cache_headers(true) -- Cacheable: explicit IP in path | ||
|
|
||
| -- Get the IP we want | ||
| record = ngx_vars.ip | ||
| local record = ngx_vars.ip | ||
|
|
||
| local ptr = getptr(record) | ||
| local ptr = utils.get_ptr(record) | ||
| ngx.say(ptr) |
There was a problem hiding this comment.
This location always sets set_cache_headers(true) for path-based PTR lookups, even if utils.get_ptr(record) returns nil or an error string. That risks caching transient failures at the edge for 1 year. Consider setting cacheable headers only after confirming a successful PTR value (or using a short/non-cacheable TTL when the lookup fails).
| utils.set_cache_headers(true) -- Cacheable: explicit IP in query | ||
| record = args.ip |
There was a problem hiding this comment.
args.ip is treated as cacheable input and used directly as the DNS query target. With edge caching enabled, unvalidated/high-cardinality values (including invalid IPs) can cause cache pollution and long-lived caching of error responses. Consider validating args.ip with utils.validate_ip (and returning 400 / non-cacheable headers when invalid).
| utils.set_cache_headers(true) -- Cacheable: explicit IP in query | |
| record = args.ip | |
| if utils.validate_ip(args.ip) then | |
| utils.set_cache_headers(true) -- Cacheable: explicit IP in query | |
| record = args.ip | |
| else | |
| utils.set_cache_headers(false) -- Not cacheable: invalid IP in query | |
| ngx.status = ngx.HTTP_BAD_REQUEST | |
| ngx.say("Invalid IP address") | |
| return | |
| end |
| if args.ip then | ||
| utils.set_cache_headers(true) -- Cacheable: explicit IP in query | ||
| local ips = {} | ||
| local msg = "" | ||
| local info = "" | ||
| --- Put our IPs into a table | ||
| ips = split(args.ip, ',') | ||
| ips = utils.split(args.ip, ',') | ||
| for k,v in ipairs(ips) do |
There was a problem hiding this comment.
For ?ip= requests you now mark the response cacheable at the edge, but the code accepts arbitrary comma-separated values without validation/normalization. That can lead to caching long-lived responses for invalid inputs and increases cache-key cardinality (e.g., whitespace variations). Consider splitting + trimming each IP and validating via utils.validate_ip before setting cacheable headers (invalid input should return 400 and be non-cacheable).
| if args.ip then | ||
| utils.set_cache_headers(true) -- Cacheable: explicit IP in query | ||
| local ips = {} | ||
| local msg = {} | ||
| local info = "" | ||
| -- Put our IPs into a table | ||
| ips = split(args.ip, ',') | ||
| ips = utils.split(args.ip, ',') |
There was a problem hiding this comment.
Same as the country endpoint: ?ip= values are now edge-cacheable, but the list isn't trimmed/validated before lookups and caching. Consider normalizing (split/trim) and validating each IP (e.g., utils.validate_ip) before setting set_cache_headers(true); invalid input should be rejected and not cached.
Summary
Cloudflare-CDN-Cache-Controlto instruct Cloudflare CDN to cache responses at the edge for up to 1 yearCache-Control: private, no-storeto prevent stale data after cache purgesCacheable endpoints (edge TTL: 1 year)
These endpoints have deterministic responses based on the explicit IP:
/v1/ip/country/{IP},/v1/ip/geo/{IP}.json,/v1/dns/ptr/{IP}, etc.?ip=parameterNon-cacheable endpoints
These return data specific to the requester's IP:
/v1/ip,/v1/ip.json,/v1/ip.js(returns requester's IP)/v1/ip/country,/v1/ip/geo.json, etc. without IP parameterImplementation
Added a
set_cache_headers(is_cacheable)utility function that sets:Cache-Control: private, no-store(browsers never cache)Cloudflare-CDN-Cache-Control: max-age=31536000(cacheable) orprivate(non-cacheable)Test plan
Cloudflare-CDN-Cache-Control: max-age=31536000Cloudflare-CDN-Cache-Control: private