From 983ca05eec6623920d24ec0f1005f487d3734a0c Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 2 May 2025 10:46:38 +0200 Subject: [PATCH 1/8] Update VERSION for 6.4.21 --- Kernel.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Kernel.php b/Kernel.php index dd80ab6175..3ea14b47ed 100644 --- a/Kernel.php +++ b/Kernel.php @@ -76,12 +76,12 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl */ private static array $freshCache = []; - public const VERSION = '6.4.21-DEV'; + public const VERSION = '6.4.21'; public const VERSION_ID = 60421; public const MAJOR_VERSION = 6; public const MINOR_VERSION = 4; public const RELEASE_VERSION = 21; - public const EXTRA_VERSION = 'DEV'; + public const EXTRA_VERSION = ''; public const END_OF_MAINTENANCE = '11/2026'; public const END_OF_LIFE = '11/2027'; From e31903d6d4dccf51dda505c131eeb51bea407adc Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 2 May 2025 11:01:42 +0200 Subject: [PATCH 2/8] Bump Symfony version to 6.4.22 --- Kernel.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Kernel.php b/Kernel.php index 3ea14b47ed..c30785f1ba 100644 --- a/Kernel.php +++ b/Kernel.php @@ -76,12 +76,12 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl */ private static array $freshCache = []; - public const VERSION = '6.4.21'; - public const VERSION_ID = 60421; + public const VERSION = '6.4.22-DEV'; + public const VERSION_ID = 60422; public const MAJOR_VERSION = 6; public const MINOR_VERSION = 4; - public const RELEASE_VERSION = 21; - public const EXTRA_VERSION = ''; + public const RELEASE_VERSION = 22; + public const EXTRA_VERSION = 'DEV'; public const END_OF_MAINTENANCE = '11/2026'; public const END_OF_LIFE = '11/2027'; From 15c105b839a7cfa1bc0989c091bfb6477f23b673 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 29 May 2025 09:23:40 +0200 Subject: [PATCH 3/8] Update VERSION for 6.4.22 --- Kernel.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Kernel.php b/Kernel.php index c30785f1ba..bd75891730 100644 --- a/Kernel.php +++ b/Kernel.php @@ -76,12 +76,12 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl */ private static array $freshCache = []; - public const VERSION = '6.4.22-DEV'; + public const VERSION = '6.4.22'; public const VERSION_ID = 60422; public const MAJOR_VERSION = 6; public const MINOR_VERSION = 4; public const RELEASE_VERSION = 22; - public const EXTRA_VERSION = 'DEV'; + public const EXTRA_VERSION = ''; public const END_OF_MAINTENANCE = '11/2026'; public const END_OF_LIFE = '11/2027'; From 242e4b5c4283c97a941dc4251c358f6041e8e7a3 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 29 May 2025 09:34:10 +0200 Subject: [PATCH 4/8] Bump Symfony version to 6.4.23 --- Kernel.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Kernel.php b/Kernel.php index bd75891730..91c143f342 100644 --- a/Kernel.php +++ b/Kernel.php @@ -76,12 +76,12 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl */ private static array $freshCache = []; - public const VERSION = '6.4.22'; - public const VERSION_ID = 60422; + public const VERSION = '6.4.23-DEV'; + public const VERSION_ID = 60423; public const MAJOR_VERSION = 6; public const MINOR_VERSION = 4; - public const RELEASE_VERSION = 22; - public const EXTRA_VERSION = ''; + public const RELEASE_VERSION = 23; + public const EXTRA_VERSION = 'DEV'; public const END_OF_MAINTENANCE = '11/2026'; public const END_OF_LIFE = '11/2027'; From 36cae1393be89e4d520541cb84325459d6c3b598 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 29 May 2025 09:38:07 +0200 Subject: [PATCH 5/8] Bump Symfony version to 7.2.8 --- Kernel.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Kernel.php b/Kernel.php index 09b362c1ff..8948fc7533 100644 --- a/Kernel.php +++ b/Kernel.php @@ -73,12 +73,12 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl */ private static array $freshCache = []; - public const VERSION = '7.2.7'; - public const VERSION_ID = 70207; + public const VERSION = '7.2.8-DEV'; + public const VERSION_ID = 70208; public const MAJOR_VERSION = 7; public const MINOR_VERSION = 2; - public const RELEASE_VERSION = 7; - public const EXTRA_VERSION = ''; + public const RELEASE_VERSION = 8; + public const EXTRA_VERSION = 'DEV'; public const END_OF_MAINTENANCE = '07/2025'; public const END_OF_LIFE = '07/2025'; From 58a0b41ed17607521e009817db1ab593b2836b66 Mon Sep 17 00:00:00 2001 From: Santiago San Martin Date: Mon, 19 May 2025 20:36:58 -0300 Subject: [PATCH 6/8] [HttpKernel] Fix `#[MapUploadedFile]` handling for optional file uploads --- .../RequestPayloadValueResolver.php | 6 +- .../UploadedFileValueResolverTest.php | 60 +++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/Controller/ArgumentResolver/RequestPayloadValueResolver.php b/Controller/ArgumentResolver/RequestPayloadValueResolver.php index a196250e8b..3a10c9d9c7 100644 --- a/Controller/ArgumentResolver/RequestPayloadValueResolver.php +++ b/Controller/ArgumentResolver/RequestPayloadValueResolver.php @@ -232,6 +232,10 @@ private function mapRequestPayload(Request $request, ArgumentMetadata $argument, private function mapUploadedFile(Request $request, ArgumentMetadata $argument, MapUploadedFile $attribute): UploadedFile|array|null { - return $request->files->get($attribute->name ?? $argument->getName(), []); + if (!($files = $request->files->get($attribute->name ?? $argument->getName(), [])) && ($argument->isNullable() || $argument->hasDefaultValue())) { + return null; + } + + return $files; } } diff --git a/Tests/Controller/ArgumentResolver/UploadedFileValueResolverTest.php b/Tests/Controller/ArgumentResolver/UploadedFileValueResolverTest.php index 5eb0d32483..479fbf1808 100644 --- a/Tests/Controller/ArgumentResolver/UploadedFileValueResolverTest.php +++ b/Tests/Controller/ArgumentResolver/UploadedFileValueResolverTest.php @@ -307,6 +307,66 @@ static function () {}, $resolver->onKernelControllerArguments($event); } + /** + * @dataProvider provideContext + */ + public function testShouldAllowEmptyWhenNullable(RequestPayloadValueResolver $resolver, Request $request) + { + $attribute = new MapUploadedFile(); + $argument = new ArgumentMetadata( + 'qux', + UploadedFile::class, + false, + false, + null, + true, + [$attribute::class => $attribute] + ); + /** @var HttpKernelInterface&MockObject $httpKernel */ + $httpKernel = $this->createMock(HttpKernelInterface::class); + $event = new ControllerArgumentsEvent( + $httpKernel, + static function () {}, + $resolver->resolve($request, $argument), + $request, + HttpKernelInterface::MAIN_REQUEST + ); + $resolver->onKernelControllerArguments($event); + $data = $event->getArguments()[0]; + + $this->assertNull($data); + } + + /** + * @dataProvider provideContext + */ + public function testShouldAllowEmptyWhenHasDefaultValue(RequestPayloadValueResolver $resolver, Request $request) + { + $attribute = new MapUploadedFile(); + $argument = new ArgumentMetadata( + 'qux', + UploadedFile::class, + false, + true, + 'default-value', + false, + [$attribute::class => $attribute] + ); + /** @var HttpKernelInterface&MockObject $httpKernel */ + $httpKernel = $this->createMock(HttpKernelInterface::class); + $event = new ControllerArgumentsEvent( + $httpKernel, + static function () {}, + $resolver->resolve($request, $argument), + $request, + HttpKernelInterface::MAIN_REQUEST + ); + $resolver->onKernelControllerArguments($event); + $data = $event->getArguments()[0]; + + $this->assertSame('default-value', $data); + } + public static function provideContext(): iterable { $resolver = new RequestPayloadValueResolver( From 9e9c725272b51aaddb863dcd59ca3ab4070a6df3 Mon Sep 17 00:00:00 2001 From: Matthias Pigulla Date: Wed, 21 May 2025 14:18:44 +0200 Subject: [PATCH 7/8] [HttpCache] Hit the backend only once after waiting for the cache lock --- HttpCache/CacheWasLockedException.php | 19 +++++++++ HttpCache/HttpCache.php | 18 ++++---- Tests/HttpCache/HttpCacheTest.php | 60 +++++++++++++++++++++++++++ Tests/HttpCache/HttpCacheTestCase.php | 11 ++++- 4 files changed, 96 insertions(+), 12 deletions(-) create mode 100644 HttpCache/CacheWasLockedException.php diff --git a/HttpCache/CacheWasLockedException.php b/HttpCache/CacheWasLockedException.php new file mode 100644 index 0000000000..f13946ad71 --- /dev/null +++ b/HttpCache/CacheWasLockedException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\HttpCache; + +/** + * @internal + */ +class CacheWasLockedException extends \Exception +{ +} diff --git a/HttpCache/HttpCache.php b/HttpCache/HttpCache.php index 3b484e5c3e..bce0e99b5e 100644 --- a/HttpCache/HttpCache.php +++ b/HttpCache/HttpCache.php @@ -219,7 +219,13 @@ public function handle(Request $request, int $type = HttpKernelInterface::MAIN_R $this->record($request, 'reload'); $response = $this->fetch($request, $catch); } else { - $response = $this->lookup($request, $catch); + $response = null; + do { + try { + $response = $this->lookup($request, $catch); + } catch (CacheWasLockedException) { + } + } while (null === $response); } $this->restoreResponseBody($request, $response); @@ -576,15 +582,7 @@ protected function lock(Request $request, Response $entry): bool // wait for the lock to be released if ($this->waitForLock($request)) { - // replace the current entry with the fresh one - $new = $this->lookup($request); - $entry->headers = $new->headers; - $entry->setContent($new->getContent()); - $entry->setStatusCode($new->getStatusCode()); - $entry->setProtocolVersion($new->getProtocolVersion()); - foreach ($new->headers->getCookies() as $cookie) { - $entry->headers->setCookie($cookie); - } + throw new CacheWasLockedException(); // unwind back to handle(), try again } else { // backend is slow as hell, send a 503 response (to avoid the dog pile effect) $entry->setStatusCode(503); diff --git a/Tests/HttpCache/HttpCacheTest.php b/Tests/HttpCache/HttpCacheTest.php index a72c08b872..39f00a0139 100644 --- a/Tests/HttpCache/HttpCacheTest.php +++ b/Tests/HttpCache/HttpCacheTest.php @@ -18,6 +18,7 @@ use Symfony\Component\HttpKernel\Event\TerminateEvent; use Symfony\Component\HttpKernel\HttpCache\Esi; use Symfony\Component\HttpKernel\HttpCache\HttpCache; +use Symfony\Component\HttpKernel\HttpCache\Store; use Symfony\Component\HttpKernel\HttpCache\StoreInterface; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\Kernel; @@ -717,6 +718,7 @@ public function testDegradationWhenCacheLocked() */ sleep(10); + $this->store = $this->createStore(); // create another store instance that does not hold the current lock $this->request('GET', '/'); $this->assertHttpKernelIsNotCalled(); $this->assertEquals(200, $this->response->getStatusCode()); @@ -735,6 +737,64 @@ public function testDegradationWhenCacheLocked() $this->assertEquals('Old response', $this->response->getContent()); } + public function testHitBackendOnlyOnceWhenCacheWasLocked() + { + // Disable stale-while-revalidate, it circumvents waiting for the lock + $this->cacheConfig['stale_while_revalidate'] = 0; + + $this->setNextResponses([ + [ + 'status' => 200, + 'body' => 'initial response', + 'headers' => [ + 'Cache-Control' => 'public, no-cache', + 'Last-Modified' => 'some while ago', + ], + ], + [ + 'status' => 304, + 'body' => '', + 'headers' => [ + 'Cache-Control' => 'public, no-cache', + 'Last-Modified' => 'some while ago', + ], + ], + [ + 'status' => 500, + 'body' => 'The backend should not be called twice during revalidation', + 'headers' => [], + ], + ]); + + $this->request('GET', '/'); // warm the cache + + // Use a store that simulates a cache entry being locked upon first attempt + $this->store = new class(sys_get_temp_dir() . '/http_cache') extends Store { + private bool $hasLock = false; + + public function lock(Request $request): bool + { + $hasLock = $this->hasLock; + $this->hasLock = true; + + return $hasLock; + } + + public function isLocked(Request $request): bool + { + return false; + } + }; + + $this->request('GET', '/'); // hit the cache with simulated lock/concurrency block + + $this->assertEquals(200, $this->response->getStatusCode()); + $this->assertEquals('initial response', $this->response->getContent()); + + $traces = $this->cache->getTraces(); + $this->assertSame(['stale', 'valid', 'store'], current($traces)); + } + public function testHitsCachedResponseWithSMaxAgeDirective() { $time = \DateTimeImmutable::createFromFormat('U', time() - 5); diff --git a/Tests/HttpCache/HttpCacheTestCase.php b/Tests/HttpCache/HttpCacheTestCase.php index 26a29f16b2..88f6bed56f 100644 --- a/Tests/HttpCache/HttpCacheTestCase.php +++ b/Tests/HttpCache/HttpCacheTestCase.php @@ -30,7 +30,7 @@ abstract class HttpCacheTestCase extends TestCase protected $responses; protected $catch; protected $esi; - protected Store $store; + protected ?Store $store = null; protected function setUp(): void { @@ -115,7 +115,9 @@ public function request($method, $uri = '/', $server = [], $cookies = [], $esi = $this->kernel->reset(); - $this->store = new Store(sys_get_temp_dir().'/http_cache'); + if (! $this->store) { + $this->store = $this->createStore(); + } if (!isset($this->cacheConfig['debug'])) { $this->cacheConfig['debug'] = true; @@ -183,4 +185,9 @@ public static function clearDirectory($directory) closedir($fp); } + + protected function createStore(): Store + { + return new Store(sys_get_temp_dir() . '/http_cache'); + } } From 3f461ff845e4529e0fa14f9639a7b6898367aa34 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 28 Jun 2025 10:20:39 +0200 Subject: [PATCH 8/8] Update VERSION for 7.2.8 --- Kernel.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Kernel.php b/Kernel.php index 8948fc7533..12ec6da633 100644 --- a/Kernel.php +++ b/Kernel.php @@ -73,12 +73,12 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl */ private static array $freshCache = []; - public const VERSION = '7.2.8-DEV'; + public const VERSION = '7.2.8'; public const VERSION_ID = 70208; public const MAJOR_VERSION = 7; public const MINOR_VERSION = 2; public const RELEASE_VERSION = 8; - public const EXTRA_VERSION = 'DEV'; + public const EXTRA_VERSION = ''; public const END_OF_MAINTENANCE = '07/2025'; public const END_OF_LIFE = '07/2025';