From ebc713bc6e6f4b53f46539fc158be85dfcd77304 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Fri, 2 Feb 2024 16:32:09 +0100 Subject: [PATCH] [HttpFoundation] Prevent duplicated headers when using Early Hints --- Response.php | 24 +++++++------- .../response-functional/early_hints.php | 31 +++++++++++++++++++ Tests/ResponseFunctionalTest.php | 28 ++++++++++++++++- 3 files changed, 69 insertions(+), 14 deletions(-) create mode 100644 Tests/Fixtures/response-functional/early_hints.php diff --git a/Response.php b/Response.php index d67c8f726..a43e7a9ac 100644 --- a/Response.php +++ b/Response.php @@ -355,23 +355,21 @@ public function sendHeaders(/* int $statusCode = null */): static $replace = false; // As recommended by RFC 8297, PHP automatically copies headers from previous 103 responses, we need to deal with that if headers changed - if (103 === $statusCode) { - $previousValues = $this->sentHeaders[$name] ?? null; - if ($previousValues === $values) { - // Header already sent in a previous response, it will be automatically copied in this response by PHP - continue; - } + $previousValues = $this->sentHeaders[$name] ?? null; + if ($previousValues === $values) { + // Header already sent in a previous response, it will be automatically copied in this response by PHP + continue; + } - $replace = 0 === strcasecmp($name, 'Content-Type'); + $replace = 0 === strcasecmp($name, 'Content-Type'); - if (null !== $previousValues && array_diff($previousValues, $values)) { - header_remove($name); - $previousValues = null; - } - - $newValues = null === $previousValues ? $values : array_diff($values, $previousValues); + if (null !== $previousValues && array_diff($previousValues, $values)) { + header_remove($name); + $previousValues = null; } + $newValues = null === $previousValues ? $values : array_diff($values, $previousValues); + foreach ($newValues as $value) { header($name.': '.$value, $replace, $this->statusCode); } diff --git a/Tests/Fixtures/response-functional/early_hints.php b/Tests/Fixtures/response-functional/early_hints.php new file mode 100644 index 000000000..90294d9ae --- /dev/null +++ b/Tests/Fixtures/response-functional/early_hints.php @@ -0,0 +1,31 @@ +headers->set('Link', '; rel="preload"; as="style"'); +$r->sendHeaders(103); + +$r->headers->set('Link', '; rel="preload"; as="script"', false); +$r->sendHeaders(103); + +$r->setContent('Hello, Early Hints'); +$r->send(); diff --git a/Tests/ResponseFunctionalTest.php b/Tests/ResponseFunctionalTest.php index ccda147df..1b3566a2c 100644 --- a/Tests/ResponseFunctionalTest.php +++ b/Tests/ResponseFunctionalTest.php @@ -12,6 +12,8 @@ namespace Symfony\Component\HttpFoundation\Tests; use PHPUnit\Framework\TestCase; +use Symfony\Component\Process\ExecutableFinder; +use Symfony\Component\Process\Process; class ResponseFunctionalTest extends TestCase { @@ -51,7 +53,31 @@ public function testCookie($fixture) public static function provideCookie() { foreach (glob(__DIR__.'/Fixtures/response-functional/*.php') as $file) { - yield [pathinfo($file, \PATHINFO_FILENAME)]; + if (str_contains($file, 'cookie')) { + yield [pathinfo($file, \PATHINFO_FILENAME)]; + } } } + + /** + * @group integration + */ + public function testInformationalResponse() + { + if (!(new ExecutableFinder())->find('curl')) { + $this->markTestSkipped('curl is not installed'); + } + + if (!($fp = @fsockopen('localhost', 80, $errorCode, $errorMessage, 2))) { + $this->markTestSkipped('FrankenPHP is not running'); + } + fclose($fp); + + $p = new Process(['curl', '-v', 'http://localhost/early_hints.php']); + $p->run(); + $output = $p->getErrorOutput(); + + $this->assertSame(3, preg_match_all('#Link: ; rel="preload"; as="style"#', $output)); + $this->assertSame(2, preg_match_all('#Link: ; rel="preload"; as="script"#', $output)); + } }