Fix Fastly domain purge, and make both cache adapters consistent - #6
Merged
Conversation
4 tasks
Greptile SummaryThe PR corrects Fastly domain purging by mapping domains to surrogate keys and adds explicit whole-zone purge operations for Fastly and Cloudflare.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains. Important Files Changed
Reviews (8): Last reviewed commit: "Correct the adapter comments and docs" | Re-trigger Greptile |
Meldiron
force-pushed
the
feat-fastly-domain-key
branch
from
August 13, 2026 18:21
c8d467b to
bfad39e
Compare
purgeDomain() ran purge_all on Fastly and never sent the domain: the argument was validated and discarded, so two different domains produced byte-identical requests. On a service fronting many domains that evicts all of them, and Fastly documents purge_all as taking up to two minutes, spiking origin traffic, and being incompatible with soft purge -- so a caller asking for a soft purge of one domain got a hard purge of everything. Fastly has no purge-by-host operation; its API offers URL, surrogate key and whole-service purges and nothing else. A domain is therefore the surrogate key the origin attaches, and the adapter has to know how those keys are named, so domainKeyPrefix is required rather than optional. There is no configuration in which purgeDomain() means purge_all. Cloudflare needed no such fix -- it purges a hostname natively -- but it was missing a zone purge entirely. Cache\Adapter now declares purgeZone() alongside the other three, so both providers offer the same set and Cache exposes it. Naming is aligned across adapters: purgeZone() for the widest purge either provider has, and PATHS_PER_PURGE / KEYS_PER_PURGE for the per-request ceilings, provider-specific numbers behind identical names. purgeKeys() on Fastly uses the batch endpoint: keys in the request body, up to 256 per request rather than one request each. A body also means no percent-encoding, which would purge a key the origin never attached. tests/Cdn/Cache/AdapterTest.php asserts the contract itself, so a provider growing a purge the others lack, or spelling one differently, fails there rather than in a caller that assumed they behaved alike. Requests are byte-identical to main for Cloudflare's three operations and Fastly's path purge, headers included. Refs Fastly purging API and purging concepts, Cloudflare purge cache docs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Meldiron
force-pushed
the
feat-fastly-domain-key
branch
from
August 13, 2026 18:59
564938a to
b4e4afd
Compare
Audit of what the diff actually claims, against the code and the provider documentation. - Fastly::PATHS_PER_PURGE was read by nothing but a test. A URL purge takes one URL, so there is no ceiling to name; only Cloudflare batches paths. - Cloudflare's batch constants claimed the provider's pages disagree about the ceiling. They do about hostnames and prefixes, not about the URLs and tags this adapter batches: those are documented at 100 per request, 500 on Enterprise for URLs. 30 is a conservative default and, more to the point, the number this adapter already sent -- the constants name existing behaviour rather than change it. - purgeZone() said the purge is disruptive to origin "for both providers". Fastly documents that; for Cloudflare it was my inference. Replaced with the consequence that holds either way. - The README still advertised three purge modes while listing four. - The Fastly note omitted zone purges from the operations that need a service ID. - Dropped a "now" that dated a comment to this change rather than describing the code, and restored the empty-paths guard so both adapters read alike. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
TorstenDittmann
approved these changes
Aug 13, 2026
Meldiron
added a commit
that referenced
this pull request
Aug 13, 2026
Provider selection lived in every consumer: read the env, decide which of
Fastly and Cloudflare applies, build one Cache per provider, call a
different purge method on each. That logic is the same everywhere and is
wrong in the same way everywhere, so it moves here.
Cache\Adapter\Balancer takes a utopia-php/balancer Balancer and purges
through every option its filters leave standing, attempting each
independently and aggregating failures into Exception\Purge, so one
provider outage cannot silently skip the rest. No option matching the
filters raises Exception\Configuration rather than passing quietly.
Extend\CdnOption wraps a balancer Option with typed accessors, so a filter
reads getProvider()/isEdge() instead of getState('provider'), and carries
the provider names as constants rather than a separate enum class.
purgeZone() is implemented too, since #6 put it on the interface: it fans
out like the others, which makes it the widest purge available here, and
only the filters keep it away from the options they exclude. AdapterTest
now covers this adapter as well, so a composite that lagged the interface
would fail there rather than silently stop forwarding an operation.
Requires utopia-php/balancer 0.4.1 for getFilteredOptions(): run() returns
one option and $filters is private, while a purge has to reach every
provider that may hold a response for the domain.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Fixes a real defect in
Fastly::purgeDomain(), applies the same scrutiny to Cloudflare, and moves the part both providers repeat into one place. Targetsmainand is independent of #5 — that PR only adds files and touches no existing adapter, so the two can merge in either order.Everything below comes from reading the provider docs rather than from assumption: Fastly purge API, Fastly purging concepts, Cloudflare purge API, Cloudflare purge cache.
1.
Fastly::purgeDomain()never purged a domainOn
mainthe argument is validated and then discarded:Two different domains therefore produce byte-identical requests:
On a service fronting many domains that evicts all of them. And
purge_allis no mild substitute: Fastly documents it as taking up to two minutes, warns that "purging a large amount of content from a high traffic service is likely to result in a rapid increase in traffic to origin", and states it "is not compatible with soft purge or bulk purge" — sosoftPurge: truewas silently ignored here. A caller asking to soft purge one domain got a hard purge of everything.Fastly has no purge-by-host operation. Its API is URL purge, surrogate key purge (single or batch), and purge-all — nothing in between. A domain is therefore addressed by the surrogate key the origin attaches, which is Fastly's own recommendation: surrogate key purges "can be used in place of single URL purge and purge-all".
domainKeyPrefixis therefore required, and there is no longer any configuration in whichpurgeDomain()meanspurge_all. Pass''when the key is the bare hostname.2. Cloudflare needed no such fix, and gained what it was missing
Cloudflare purges a hostname natively (
{"hosts": [...]}), sopurgeDomain()already sent the domain rather than discarding it. Worth stating plainly: the bug was Fastly's, not a shared design flaw.What Cloudflare was missing is a zone-wide purge —
purge_everythingwas not implemented at all. It is nowpurgeZone().Its batch ceiling is also now configurable, because Cloudflare's own pages disagree: the purge overview tables say 100 operations per request for tags/hostnames/prefixes (and 100 URLs for single-file purge, 500 on Enterprise), while the purge-by-hostname page says 30 hostnames at a time. The default stays at the lower figure, which is within both readings;
itemsPerPurgeraises it. Also relevant to batching: Cloudflare's Free tier allows only 5 purge requests per minute.3.
purge_all/purge_everythingbecomespurgeZone()on bothRenamed from
purgeService()per review. Both adapters use the same name, and both keep it off theAdapterinterface, so a routing adapter can never reach it by fanning out an interface method — a test asserts its absence from the interface.4. A shared base so bulk operations and provider-specific methods are easy to add
Cache\Adapter\Apiowns the sequence both providers repeat — authenticate a request, send it, decide whether the answer means success, turn a failure into a message. Each adapter supplies only those four pieces plus aUSER_AGENT, and an operation becomes the request it makes:Both adapters previously carried their own copy of the request-building, JSON decoding and error-formatting code. Per-request ceilings are now named constants (
Fastly::KEYS_PER_PURGE,Cloudflare::ITEMS_PER_PURGE) rather than inline magic numbers.Fastly::purgeKeys()uses this to adopt the batch endpoint:POST /service/{sid}/purgewith{"surrogate_keys": [...]}, up to 256 keys per request instead of one request per key. Soft purge still applies — Fastly: "Single object, surrogate key, and bulk surrogate key purges all support soft purge". Keys in a body also need no percent-encoding, which would purge a key the origin never attached.Test Plan
composer test— 40 tests, 71 assertions, from 26/49 onmain.composer analyse(level 6)[OK] No errors;composer lintpasses.The extraction changed no wire behaviour. I captured method, URL, every header and body for Cloudflare's three operations and Fastly's path purge on
mainand on this branch, and diffed:IDENTICAL: no wire change for these operations. Only Fastly's key and domain purges differ, which is the point of the PR.Three behaviours seen red before being claimed:
purgeDomain()pointed back atpurgeZone()Failures: 2, printing+'…/service/shared-service/purge_all'— the original bugrawurlencodere-added to the batch bodyFailures: 1,-'domain-example.com-summer sale'/+'…summer%20sale'purgeZone()sendinghostsinstead ofpurge_everythingFailures: 1,-'purge_everything' => trueisSuccess()reduced to the HTTP statusFailures: 1— a{"success":false}body stops being caughtNew coverage: Fastly domain purge as exactly one key-purge request; an empty prefix yielding the bare hostname; keys asserted unencoded; 257 keys becoming 2 requests not 257;
purgeZone()on both adapters;purgeZoneabsent from the interface; zone purge requiring a service ID; Cloudflare batching URLs and tags with the boundary asserted; a configurable ceiling collapsing 100 tags into one request; Cloudflare rejecting a 2xx whose body sayssuccess: false; empty purges touching nothing.Every README example for both providers was extracted from the markdown and executed against this branch, so the reordered Fastly constructor and the new Cloudflare snippets are known to resolve and construct rather than merely to look right.
Not verified against the live Fastly or Cloudflare APIs — coverage is at request-construction level throughout, as with the rest of the suite.
Breaking changes
Fastly:domainKeyPrefixis a required second constructor argument, so positional calls shift.Fastly::purgeDomain()no longer purges the whole service; callers who wanted that callpurgeZone().Fastly::purgeKeys()sends raw keys in a body, so callers that pre-encoded keys must stop.Cache\Adapter\Api; anyone subclassing them will see the new abstract methods.Cloudflare's constructor gains a trailingitemsPerPurge; existing calls are unaffected.Library is
0.0.x. The only consumer I know of is appwrite-labs/cloud#5259, which passes named arguments, calls neitherpurgeZone(), and is updated for the Fastly wire format.Related PRs and Issues
Split out of #5, then widened after reading the provider docs. Both are needed by appwrite-labs/cloud#5259, which tracks the
integration/balancer-and-fastlybranch (a merge of the two) until they are merged and tagged.Checklist
composer lintcomposer analysecomposer test🤖 Generated with Claude Code