Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions src/Cdn/Cache/Adapter/Cloudflare.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,16 +95,25 @@ private function send(array $body): void
}

/**
* A 2xx is not enough: Cloudflare reports a rejected purge in the body.
* The status line is the primary signal, as it is for Fastly. Cloudflare's
* envelope carries its own verdict, and an explicit `success: false` inside
* a 2xx is still a rejected purge — but a 2xx without the envelope is
* acceptance, so a body a gateway rewrote or a test double never sent does
* not fail a purge the status line already confirmed.
*
* @param array{statusCode:int,response:array<string, mixed>|string|null,error:string|null} $result
*/
private function isSuccess(array $result): bool
{
return $result['statusCode'] >= 200
&& $result['statusCode'] < 300
&& \is_array($result['response'])
&& ($result['response']['success'] ?? false) === true;
if ($result['statusCode'] < 200 || $result['statusCode'] >= 300) {
return false;
}

if (\is_array($result['response']) && \array_key_exists('success', $result['response'])) {
return $result['response']['success'] === true;
}

return true;
}

/**
Expand Down
23 changes: 22 additions & 1 deletion tests/Cdn/Cache/Adapter/CloudflareTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,35 @@ public function testZonePurgeIsItsOwnOperation(): void

public function testRejectsAPurgeTheBodyReportsAsFailed(): void
{
// A 2xx alone does not mean the purge happened.
// The envelope's explicit verdict wins over the 2xx it travels in.
$client = new TestClient([new Response(200, body: new Stream('{"success":false,"errors":[{"message":"Invalid zone"}]}'))]);

$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('Cloudflare purge failed with status 200: Invalid zone');
(new Cloudflare('zone', 'token', $client))->purgeDomain('example.com');
}

public function testAcceptsAPurgeWithoutTheEnvelope(): void
{
// A 2xx with no envelope is acceptance, like Fastly: the status line is
// the primary signal and only an explicit `success: false` overrides it.
$client = new TestClient([new Response(200, body: new Stream(''))]);

(new Cloudflare('zone', 'token', $client))->purgeDomain('example.com');

$this->assertSame(['hosts' => ['example.com']], $client->calls[0]['body']);
}

public function testRejectsAPurgeTheStatusReportsAsFailed(): void
{
// The reverse does not hold: no envelope softens a failing status.
$client = new TestClient([new Response(403, body: new Stream('{"success":false,"errors":[{"message":"Forbidden"}]}'))]);

$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('Cloudflare purge failed with status 403: Forbidden');
(new Cloudflare('zone', 'token', $client))->purgeDomain('example.com');
}

public function testEmptyPurgesTouchNothing(): void
{
$client = new TestClient([]);
Expand Down
Loading