Skip to content

Commit

Permalink
[HttpFoundation] Prevent duplicated headers when using Early Hints
Browse files Browse the repository at this point in the history
  • Loading branch information
dunglas authored and nicolas-grekas committed Feb 8, 2024
1 parent 7cda7b4 commit ca5db79
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 14 deletions.
10 changes: 10 additions & 0 deletions .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,16 @@ jobs:
KAFKA_ADVERTISED_HOST_NAME: 127.0.0.1
KAFKA_ZOOKEEPER_CONNECT: 'zookeeper:2181'
KAFKA_ADVERTISED_PORT: 9092
frankenphp:
image: dunglas/frankenphp:1.1.0
ports:
- 80:80
volumes:
- ${{ github.workspace }}:/symfony
env:
SERVER_NAME: 'http://localhost'
CADDY_SERVER_EXTRA_DIRECTIVES: |
root * /symfony/src/Symfony/Component/HttpFoundation/Tests/Fixtures/response-functional/
steps:
- name: Checkout
Expand Down
24 changes: 11 additions & 13 deletions src/Symfony/Component/HttpFoundation/Response.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

// Requires FrankenPHP

use Symfony\Component\HttpFoundation\Response;

$parent = __DIR__;
while (!@file_exists($parent.'/vendor/autoload.php')) {
if (!@file_exists($parent)) {
// open_basedir restriction in effect
break;
}
if ($parent === dirname($parent)) {
echo "vendor/autoload.php not found\n";
exit(1);
}

$parent = dirname($parent);
}

require $parent.'/vendor/autoload.php';

$r = new Response();
$r->headers->set('Link', '</css/style.css>; rel="preload"; as="style"');
$r->sendHeaders(103);

$r->headers->set('Link', '</js/app.js>; rel="preload"; as="script"', false);
$r->sendHeaders(103);

$r->setContent('Hello, Early Hints');
$r->send();
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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: </css/style\.css>; rel="preload"; as="style"#', $output));
$this->assertSame(2, preg_match_all('#Link: </js/app\.js>; rel="preload"; as="script"#', $output));
}
}

0 comments on commit ca5db79

Please sign in to comment.