Merge branch '3.4' into 4.3
* 3.4: [Validator] fix access to uninitialized property when getting value [HttpKernel] Fix stale-if-error behavior, add tests Improved error message when no supported user provider is found
This commit is contained in:
commit
0f13d5a5fc
@ -628,7 +628,7 @@ class Response
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the response must be revalidated by caches.
|
* Returns true if the response must be revalidated by shared caches once it has become stale.
|
||||||
*
|
*
|
||||||
* This method indicates that the response must not be served stale by a
|
* This method indicates that the response must not be served stale by a
|
||||||
* cache in any circumstance without first revalidating with the origin.
|
* cache in any circumstance without first revalidating with the origin.
|
||||||
|
@ -482,13 +482,37 @@ class HttpCache implements HttpKernelInterface, TerminableInterface
|
|||||||
// always a "master" request (as the real master request can be in cache)
|
// always a "master" request (as the real master request can be in cache)
|
||||||
$response = SubRequestHandler::handle($this->kernel, $request, HttpKernelInterface::MASTER_REQUEST, $catch);
|
$response = SubRequestHandler::handle($this->kernel, $request, HttpKernelInterface::MASTER_REQUEST, $catch);
|
||||||
|
|
||||||
// we don't implement the stale-if-error on Requests, which is nonetheless part of the RFC
|
/*
|
||||||
if (null !== $entry && \in_array($response->getStatusCode(), [500, 502, 503, 504])) {
|
* Support stale-if-error given on Responses or as a config option.
|
||||||
|
* RFC 7234 summarizes in Section 4.2.4 (but also mentions with the individual
|
||||||
|
* Cache-Control directives) that
|
||||||
|
*
|
||||||
|
* A cache MUST NOT generate a stale response if it is prohibited by an
|
||||||
|
* explicit in-protocol directive (e.g., by a "no-store" or "no-cache"
|
||||||
|
* cache directive, a "must-revalidate" cache-response-directive, or an
|
||||||
|
* applicable "s-maxage" or "proxy-revalidate" cache-response-directive;
|
||||||
|
* see Section 5.2.2).
|
||||||
|
*
|
||||||
|
* https://tools.ietf.org/html/rfc7234#section-4.2.4
|
||||||
|
*
|
||||||
|
* We deviate from this in one detail, namely that we *do* serve entries in the
|
||||||
|
* stale-if-error case even if they have a `s-maxage` Cache-Control directive.
|
||||||
|
*/
|
||||||
|
if (null !== $entry
|
||||||
|
&& \in_array($response->getStatusCode(), [500, 502, 503, 504])
|
||||||
|
&& !$entry->headers->hasCacheControlDirective('no-cache')
|
||||||
|
&& !$entry->mustRevalidate()
|
||||||
|
) {
|
||||||
if (null === $age = $entry->headers->getCacheControlDirective('stale-if-error')) {
|
if (null === $age = $entry->headers->getCacheControlDirective('stale-if-error')) {
|
||||||
$age = $this->options['stale_if_error'];
|
$age = $this->options['stale_if_error'];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (abs($entry->getTtl()) < $age) {
|
/*
|
||||||
|
* stale-if-error gives the (extra) time that the Response may be used *after* it has become stale.
|
||||||
|
* So we compare the time the $entry has been sitting in the cache already with the
|
||||||
|
* time it was fresh plus the allowed grace period.
|
||||||
|
*/
|
||||||
|
if ($entry->getAge() <= $entry->getMaxAge() + $age) {
|
||||||
$this->record($request, 'stale-if-error');
|
$this->record($request, 'stale-if-error');
|
||||||
|
|
||||||
return $entry;
|
return $entry;
|
||||||
|
@ -1524,6 +1524,168 @@ class HttpCacheTest extends HttpCacheTestCase
|
|||||||
$cache->handle($request, HttpKernelInterface::SUB_REQUEST);
|
$cache->handle($request, HttpKernelInterface::SUB_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testStaleIfErrorMustNotResetLifetime()
|
||||||
|
{
|
||||||
|
// Make sure we don't accidentally treat the response as fresh (revalidated) again
|
||||||
|
// when stale-if-error handling kicks in.
|
||||||
|
|
||||||
|
$responses = [
|
||||||
|
[
|
||||||
|
'status' => 200,
|
||||||
|
'body' => 'OK',
|
||||||
|
// This is cacheable and can be used in stale-if-error cases:
|
||||||
|
'headers' => ['Cache-Control' => 'public, max-age=10', 'ETag' => 'some-etag'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'status' => 500,
|
||||||
|
'body' => 'FAIL',
|
||||||
|
'headers' => [],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'status' => 500,
|
||||||
|
'body' => 'FAIL',
|
||||||
|
'headers' => [],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->setNextResponses($responses);
|
||||||
|
$this->cacheConfig['stale_if_error'] = 10;
|
||||||
|
|
||||||
|
$this->request('GET', '/'); // warm cache
|
||||||
|
|
||||||
|
sleep(15); // now the entry is stale, but still within the grace period (10s max-age + 10s stale-if-error)
|
||||||
|
|
||||||
|
$this->request('GET', '/'); // hit backend error
|
||||||
|
$this->assertEquals(200, $this->response->getStatusCode()); // stale-if-error saved the day
|
||||||
|
$this->assertEquals(15, $this->response->getAge());
|
||||||
|
|
||||||
|
sleep(10); // now we're outside the grace period
|
||||||
|
|
||||||
|
$this->request('GET', '/'); // hit backend error
|
||||||
|
$this->assertEquals(500, $this->response->getStatusCode()); // fail
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider getResponseDataThatMayBeServedStaleIfError
|
||||||
|
*/
|
||||||
|
public function testResponsesThatMayBeUsedStaleIfError($responseHeaders, $sleepBetweenRequests = null)
|
||||||
|
{
|
||||||
|
$responses = [
|
||||||
|
[
|
||||||
|
'status' => 200,
|
||||||
|
'body' => 'OK',
|
||||||
|
'headers' => $responseHeaders,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'status' => 500,
|
||||||
|
'body' => 'FAIL',
|
||||||
|
'headers' => [],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->setNextResponses($responses);
|
||||||
|
$this->cacheConfig['stale_if_error'] = 10; // after stale, may be served for 10s
|
||||||
|
|
||||||
|
$this->request('GET', '/'); // warm cache
|
||||||
|
|
||||||
|
if ($sleepBetweenRequests) {
|
||||||
|
sleep($sleepBetweenRequests);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->request('GET', '/'); // hit backend error
|
||||||
|
|
||||||
|
$this->assertEquals(200, $this->response->getStatusCode());
|
||||||
|
$this->assertEquals('OK', $this->response->getContent());
|
||||||
|
$this->assertTraceContains('stale-if-error');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getResponseDataThatMayBeServedStaleIfError()
|
||||||
|
{
|
||||||
|
// All data sets assume that a 10s stale-if-error grace period has been configured
|
||||||
|
yield 'public, max-age expired' => [['Cache-Control' => 'public, max-age=60'], 65];
|
||||||
|
yield 'public, validateable with ETag, no TTL' => [['Cache-Control' => 'public', 'ETag' => 'some-etag'], 5];
|
||||||
|
yield 'public, validateable with Last-Modified, no TTL' => [['Cache-Control' => 'public', 'Last-Modified' => 'yesterday'], 5];
|
||||||
|
yield 'public, s-maxage will be served stale-if-error, even if the RFC mandates otherwise' => [['Cache-Control' => 'public, s-maxage=20'], 25];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider getResponseDataThatMustNotBeServedStaleIfError
|
||||||
|
*/
|
||||||
|
public function testResponsesThatMustNotBeUsedStaleIfError($responseHeaders, $sleepBetweenRequests = null)
|
||||||
|
{
|
||||||
|
$responses = [
|
||||||
|
[
|
||||||
|
'status' => 200,
|
||||||
|
'body' => 'OK',
|
||||||
|
'headers' => $responseHeaders,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'status' => 500,
|
||||||
|
'body' => 'FAIL',
|
||||||
|
'headers' => [],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->setNextResponses($responses);
|
||||||
|
$this->cacheConfig['stale_if_error'] = 10; // after stale, may be served for 10s
|
||||||
|
$this->cacheConfig['strict_smaxage'] = true; // full RFC compliance for this feature
|
||||||
|
|
||||||
|
$this->request('GET', '/'); // warm cache
|
||||||
|
|
||||||
|
if ($sleepBetweenRequests) {
|
||||||
|
sleep($sleepBetweenRequests);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->request('GET', '/'); // hit backend error
|
||||||
|
|
||||||
|
$this->assertEquals(500, $this->response->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getResponseDataThatMustNotBeServedStaleIfError()
|
||||||
|
{
|
||||||
|
// All data sets assume that a 10s stale-if-error grace period has been configured
|
||||||
|
yield 'public, no TTL but beyond grace period' => [['Cache-Control' => 'public'], 15];
|
||||||
|
yield 'public, validateable with ETag, no TTL but beyond grace period' => [['Cache-Control' => 'public', 'ETag' => 'some-etag'], 15];
|
||||||
|
yield 'public, validateable with Last-Modified, no TTL but beyond grace period' => [['Cache-Control' => 'public', 'Last-Modified' => 'yesterday'], 15];
|
||||||
|
yield 'public, stale beyond grace period' => [['Cache-Control' => 'public, max-age=10'], 30];
|
||||||
|
|
||||||
|
// Cache-control values that prohibit serving stale responses or responses without positive validation -
|
||||||
|
// see https://tools.ietf.org/html/rfc7234#section-4.2.4 and
|
||||||
|
// https://tools.ietf.org/html/rfc7234#section-5.2.2
|
||||||
|
yield 'no-cache requires positive validation' => [['Cache-Control' => 'public, no-cache', 'ETag' => 'some-etag']];
|
||||||
|
yield 'no-cache requires positive validation, even if fresh' => [['Cache-Control' => 'public, no-cache, max-age=10']];
|
||||||
|
yield 'must-revalidate requires positive validation once stale' => [['Cache-Control' => 'public, max-age=10, must-revalidate'], 15];
|
||||||
|
yield 'proxy-revalidate requires positive validation once stale' => [['Cache-Control' => 'public, max-age=10, proxy-revalidate'], 15];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testStaleIfErrorWhenStrictSmaxageDisabled()
|
||||||
|
{
|
||||||
|
$responses = [
|
||||||
|
[
|
||||||
|
'status' => 200,
|
||||||
|
'body' => 'OK',
|
||||||
|
'headers' => ['Cache-Control' => 'public, s-maxage=20'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'status' => 500,
|
||||||
|
'body' => 'FAIL',
|
||||||
|
'headers' => [],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->setNextResponses($responses);
|
||||||
|
$this->cacheConfig['stale_if_error'] = 10;
|
||||||
|
$this->cacheConfig['strict_smaxage'] = false;
|
||||||
|
|
||||||
|
$this->request('GET', '/'); // warm cache
|
||||||
|
sleep(25);
|
||||||
|
$this->request('GET', '/'); // hit backend error
|
||||||
|
|
||||||
|
$this->assertEquals(200, $this->response->getStatusCode());
|
||||||
|
$this->assertEquals('OK', $this->response->getContent());
|
||||||
|
$this->assertTraceContains('stale-if-error');
|
||||||
|
}
|
||||||
|
|
||||||
public function testTraceHeaderNameCanBeChanged()
|
public function testTraceHeaderNameCanBeChanged()
|
||||||
{
|
{
|
||||||
$this->cacheConfig['trace_header'] = 'X-My-Header';
|
$this->cacheConfig['trace_header'] = 'X-My-Header';
|
||||||
|
@ -91,7 +91,7 @@ class ChainUserProvider implements UserProviderInterface
|
|||||||
$e->setUsername($user->getUsername());
|
$e->setUsername($user->getUsername());
|
||||||
throw $e;
|
throw $e;
|
||||||
} else {
|
} else {
|
||||||
throw new UnsupportedUserException(sprintf('The account "%s" is not supported.', \get_class($user)));
|
throw new UnsupportedUserException(sprintf('There is no user provider for user "%s". Shouldn\'t the "supportsClass()" method of your user provider return true for this classname?', \get_class($user)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -255,7 +255,7 @@ class ContextListener implements ListenerInterface
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new \RuntimeException(sprintf('There is no user provider for user "%s".', $userClass));
|
throw new \RuntimeException(sprintf('There is no user provider for user "%s". Shouldn\'t the "supportsClass()" method of your user provider return true for this classname?', $userClass));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function safelyUnserialize($serializedToken)
|
private function safelyUnserialize($serializedToken)
|
||||||
|
@ -233,7 +233,7 @@ abstract class AbstractRememberMeServices implements RememberMeServicesInterface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new UnsupportedUserException(sprintf('There is no user provider that supports class "%s".', $class));
|
throw new UnsupportedUserException(sprintf('There is no user provider for user "%s". Shouldn\'t the "supportsClass()" method of your user provider return true for this classname?', $class));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -48,7 +48,13 @@ class PropertyMetadata extends MemberMetadata
|
|||||||
*/
|
*/
|
||||||
public function getPropertyValue($object)
|
public function getPropertyValue($object)
|
||||||
{
|
{
|
||||||
return $this->getReflectionMember($object)->getValue($object);
|
$reflProperty = $this->getReflectionMember($object);
|
||||||
|
|
||||||
|
if (\PHP_VERSION_ID >= 70400 && !$reflProperty->isInitialized($object)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $reflProperty->getValue($object);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Symfony\Component\Validator\Tests\Fixtures;
|
||||||
|
|
||||||
|
class Entity_74
|
||||||
|
{
|
||||||
|
public int $uninitialized;
|
||||||
|
}
|
@ -14,10 +14,12 @@ namespace Symfony\Component\Validator\Tests\Mapping;
|
|||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Symfony\Component\Validator\Mapping\PropertyMetadata;
|
use Symfony\Component\Validator\Mapping\PropertyMetadata;
|
||||||
use Symfony\Component\Validator\Tests\Fixtures\Entity;
|
use Symfony\Component\Validator\Tests\Fixtures\Entity;
|
||||||
|
use Symfony\Component\Validator\Tests\Fixtures\Entity_74;
|
||||||
|
|
||||||
class PropertyMetadataTest extends TestCase
|
class PropertyMetadataTest extends TestCase
|
||||||
{
|
{
|
||||||
const CLASSNAME = 'Symfony\Component\Validator\Tests\Fixtures\Entity';
|
const CLASSNAME = 'Symfony\Component\Validator\Tests\Fixtures\Entity';
|
||||||
|
const CLASSNAME_74 = 'Symfony\Component\Validator\Tests\Fixtures\Entity_74';
|
||||||
const PARENTCLASS = 'Symfony\Component\Validator\Tests\Fixtures\EntityParent';
|
const PARENTCLASS = 'Symfony\Component\Validator\Tests\Fixtures\EntityParent';
|
||||||
|
|
||||||
public function testInvalidPropertyName()
|
public function testInvalidPropertyName()
|
||||||
@ -53,4 +55,15 @@ class PropertyMetadataTest extends TestCase
|
|||||||
$this->expectException('Symfony\Component\Validator\Exception\ValidatorException');
|
$this->expectException('Symfony\Component\Validator\Exception\ValidatorException');
|
||||||
$metadata->getPropertyValue($entity);
|
$metadata->getPropertyValue($entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @requires PHP 7.4
|
||||||
|
*/
|
||||||
|
public function testGetPropertyValueFromUninitializedProperty()
|
||||||
|
{
|
||||||
|
$entity = new Entity_74();
|
||||||
|
$metadata = new PropertyMetadata(self::CLASSNAME_74, 'uninitialized');
|
||||||
|
|
||||||
|
$this->assertNull($metadata->getPropertyValue($entity));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user