Skip to content

Commit 6962aba

Browse files
committed
invalidate cache on unsafe methods only if response returns a non error status code
1 parent 9a54967 commit 6962aba

File tree

2 files changed

+65
-5
lines changed

2 files changed

+65
-5
lines changed

src/Symfony/Component/HttpClient/CachingHttpClient.php

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -154,12 +154,23 @@ public function request(string $method, string $url, array $options = []): Respo
154154
$fullUrl = implode('', $fullUrl);
155155
$fullUrlTag = self::hash($fullUrl);
156156

157-
if (\in_array($method, self::UNSAFE_METHODS, true)) {
158-
$this->cache->invalidateTags([$fullUrlTag]);
159-
}
160-
161157
if ('' !== $options['body'] || ($options['extra']['no_cache'] ?? false) || isset($options['normalized_headers']['range']) || !\in_array($method, self::CACHEABLE_METHODS, true)) {
162-
return new AsyncResponse($this->client, $method, $url, $options);
158+
return new AsyncResponse($this->client, $method, $url, $options, function (ChunkInterface $chunk, AsyncContext $context) use ($method, $fullUrlTag): \Generator {
159+
if (null !== $chunk->getError() || $chunk->isTimeout() || !$chunk->isFirst()) {
160+
yield $chunk;
161+
162+
return;
163+
}
164+
165+
$statusCode = $context->getStatusCode();
166+
if ($statusCode >= 100 && $statusCode < 400 && \in_array($method, self::UNSAFE_METHODS, true)) {
167+
$this->cache->invalidateTags([$fullUrlTag]);
168+
}
169+
170+
$context->passthru();
171+
172+
yield $chunk;
173+
});
163174
}
164175

165176
$requestHash = self::hash($method.$fullUrl.serialize(array_intersect_key($options['normalized_headers'], self::RESPONSE_INFLUENCING_HEADERS)));

src/Symfony/Component/HttpClient/Tests/CachingHttpClientTest.php

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -894,4 +894,53 @@ public function testResponseInfluencingHeadersAffectCacheKey()
894894
self::assertSame(200, $response->getStatusCode());
895895
self::assertSame('response for fr', $response->getContent());
896896
}
897+
898+
public function testUnsafeInvalidationInBypassFlow()
899+
{
900+
$mockClient = new MockHttpClient([
901+
new MockResponse('initial get', ['http_code' => 200, 'response_headers' => ['Cache-Control' => 'max-age=300']]),
902+
new MockResponse('', ['http_code' => 204]),
903+
new MockResponse('after invalidate', ['http_code' => 200]),
904+
]);
905+
906+
$client = new CachingHttpClient($mockClient, $this->cacheAdapter);
907+
908+
// Warm cache with GET
909+
$response = $client->request('GET', 'http://example.com/unsafe-test');
910+
self::assertSame(200, $response->getStatusCode());
911+
self::assertSame('initial get', $response->getContent());
912+
913+
// Unsafe POST with body (bypasses cache but invalidates on success)
914+
$client->request('POST', 'http://example.com/unsafe-test', ['body' => 'invalidate']);
915+
916+
// Next GET should miss cache and fetch new
917+
$response = $client->request('GET', 'http://example.com/unsafe-test');
918+
self::assertSame(200, $response->getStatusCode());
919+
self::assertSame('after invalidate', $response->getContent());
920+
}
921+
922+
public function testNoInvalidationOnErrorInBypassFlow()
923+
{
924+
$mockClient = new MockHttpClient([
925+
new MockResponse('initial get', ['http_code' => 200, 'response_headers' => ['Cache-Control' => 'max-age=300']]),
926+
new MockResponse('server error', ['http_code' => 500]),
927+
new MockResponse('should not be fetched'),
928+
]);
929+
930+
$client = new CachingHttpClient($mockClient, $this->cacheAdapter);
931+
932+
// Warm cache with GET
933+
$response = $client->request('GET', 'http://example.com/no-invalidate-test');
934+
self::assertSame(200, $response->getStatusCode());
935+
self::assertSame('initial get', $response->getContent());
936+
937+
// Unsafe POST with body (bypasses cache, but 500 shouldn't invalidate)
938+
$response = $client->request('POST', 'http://example.com/no-invalidate-test', ['body' => 'no invalidate']);
939+
self::assertSame(500, $response->getStatusCode());
940+
941+
// Next GET should hit cache
942+
$response = $client->request('GET', 'http://example.com/no-invalidate-test');
943+
self::assertSame(200, $response->getStatusCode());
944+
self::assertSame('initial get', $response->getContent());
945+
}
897946
}

0 commit comments

Comments
 (0)