Skip to content

feat: add Cloudflare CDN edge caching for cacheable endpoints#64

Merged
jloh merged 1 commit into
mainfrom
feature/cloudflare-edge-caching
Feb 8, 2026
Merged

feat: add Cloudflare CDN edge caching for cacheable endpoints#64
jloh merged 1 commit into
mainfrom
feature/cloudflare-edge-caching

Conversation

@jloh

@jloh jloh commented Feb 8, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add cache control headers using Cloudflare-CDN-Cache-Control to instruct Cloudflare CDN to cache responses at the edge for up to 1 year
  • Ensures only endpoints with explicit IP lookups are cached (deterministic responses)
  • Endpoints using the requester's IP remain non-cacheable (dynamic responses)
  • Browsers always receive Cache-Control: private, no-store to prevent stale data after cache purges

Cacheable endpoints (edge TTL: 1 year)

These endpoints have deterministic responses based on the explicit IP:

  • Path-based: /v1/ip/country/{IP}, /v1/ip/geo/{IP}.json, /v1/dns/ptr/{IP}, etc.
  • Query-based: Any endpoint with ?ip= parameter

Non-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 parameter

Implementation

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) or private (non-cacheable)

Test plan

  • Added 20 tests for IP/country/geo caching behavior (t/v1/ip/06-caching.t)
  • Added 9 tests for DNS PTR caching behavior (t/v1/ip/07-dns-caching.t)
  • All 355 existing + new tests pass
  • Verified cacheable endpoints return Cloudflare-CDN-Cache-Control: max-age=31536000
  • Verified non-cacheable endpoints return Cloudflare-CDN-Cache-Control: private

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
@jloh jloh self-assigned this Feb 8, 2026
@jloh
jloh requested a review from Copilot February 8, 2026 00:19

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 set Cache-Control and Cloudflare-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.

Comment thread conf/v1/dns.conf
Comment on lines 9 to 17
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)

Copilot AI Feb 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment thread conf/v1/dns.conf
Comment on lines +24 to 32
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)

Copilot AI Feb 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment thread conf/v1/dns.conf
Comment on lines +10 to 11
utils.set_cache_headers(true) -- Cacheable: explicit IP in query
record = args.ip

Copilot AI Feb 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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

Copilot uses AI. Check for mistakes.
Comment thread conf/v1/ip.conf
Comment on lines 38 to 45
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

Copilot AI Feb 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment thread conf/v1/ip.conf
Comment on lines 230 to +236
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, ',')

Copilot AI Feb 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
@jloh
jloh merged commit 3c725b8 into main Feb 8, 2026
7 checks passed
@jloh
jloh deleted the feature/cloudflare-edge-caching branch February 8, 2026 00:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants