Skip to content

Commit ca5db79

Browse files
dunglasnicolas-grekas
authored andcommitted
[HttpFoundation] Prevent duplicated headers when using Early Hints
1 parent 7cda7b4 commit ca5db79

File tree

4 files changed

+79
-14
lines changed

4 files changed

+79
-14
lines changed

.github/workflows/integration-tests.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,16 @@ jobs:
110110
KAFKA_ADVERTISED_HOST_NAME: 127.0.0.1
111111
KAFKA_ZOOKEEPER_CONNECT: 'zookeeper:2181'
112112
KAFKA_ADVERTISED_PORT: 9092
113+
frankenphp:
114+
image: dunglas/frankenphp:1.1.0
115+
ports:
116+
- 80:80
117+
volumes:
118+
- ${{ github.workspace }}:/symfony
119+
env:
120+
SERVER_NAME: 'http://localhost'
121+
CADDY_SERVER_EXTRA_DIRECTIVES: |
122+
root * /symfony/src/Symfony/Component/HttpFoundation/Tests/Fixtures/response-functional/
113123
114124
steps:
115125
- name: Checkout

src/Symfony/Component/HttpFoundation/Response.php

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -355,23 +355,21 @@ public function sendHeaders(/* int $statusCode = null */): static
355355
$replace = false;
356356

357357
// As recommended by RFC 8297, PHP automatically copies headers from previous 103 responses, we need to deal with that if headers changed
358-
if (103 === $statusCode) {
359-
$previousValues = $this->sentHeaders[$name] ?? null;
360-
if ($previousValues === $values) {
361-
// Header already sent in a previous response, it will be automatically copied in this response by PHP
362-
continue;
363-
}
358+
$previousValues = $this->sentHeaders[$name] ?? null;
359+
if ($previousValues === $values) {
360+
// Header already sent in a previous response, it will be automatically copied in this response by PHP
361+
continue;
362+
}
364363

365-
$replace = 0 === strcasecmp($name, 'Content-Type');
364+
$replace = 0 === strcasecmp($name, 'Content-Type');
366365

367-
if (null !== $previousValues && array_diff($previousValues, $values)) {
368-
header_remove($name);
369-
$previousValues = null;
370-
}
371-
372-
$newValues = null === $previousValues ? $values : array_diff($values, $previousValues);
366+
if (null !== $previousValues && array_diff($previousValues, $values)) {
367+
header_remove($name);
368+
$previousValues = null;
373369
}
374370

371+
$newValues = null === $previousValues ? $values : array_diff($values, $previousValues);
372+
375373
foreach ($newValues as $value) {
376374
header($name.': '.$value, $replace, $this->statusCode);
377375
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
// Requires FrankenPHP
4+
5+
use Symfony\Component\HttpFoundation\Response;
6+
7+
$parent = __DIR__;
8+
while (!@file_exists($parent.'/vendor/autoload.php')) {
9+
if (!@file_exists($parent)) {
10+
// open_basedir restriction in effect
11+
break;
12+
}
13+
if ($parent === dirname($parent)) {
14+
echo "vendor/autoload.php not found\n";
15+
exit(1);
16+
}
17+
18+
$parent = dirname($parent);
19+
}
20+
21+
require $parent.'/vendor/autoload.php';
22+
23+
$r = new Response();
24+
$r->headers->set('Link', '</css/style.css>; rel="preload"; as="style"');
25+
$r->sendHeaders(103);
26+
27+
$r->headers->set('Link', '</js/app.js>; rel="preload"; as="script"', false);
28+
$r->sendHeaders(103);
29+
30+
$r->setContent('Hello, Early Hints');
31+
$r->send();

src/Symfony/Component/HttpFoundation/Tests/ResponseFunctionalTest.php

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
namespace Symfony\Component\HttpFoundation\Tests;
1313

1414
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Process\ExecutableFinder;
16+
use Symfony\Component\Process\Process;
1517

1618
class ResponseFunctionalTest extends TestCase
1719
{
@@ -51,7 +53,31 @@ public function testCookie($fixture)
5153
public static function provideCookie()
5254
{
5355
foreach (glob(__DIR__.'/Fixtures/response-functional/*.php') as $file) {
54-
yield [pathinfo($file, \PATHINFO_FILENAME)];
56+
if (str_contains($file, 'cookie')) {
57+
yield [pathinfo($file, \PATHINFO_FILENAME)];
58+
}
5559
}
5660
}
61+
62+
/**
63+
* @group integration
64+
*/
65+
public function testInformationalResponse()
66+
{
67+
if (!(new ExecutableFinder())->find('curl')) {
68+
$this->markTestSkipped('curl is not installed');
69+
}
70+
71+
if (!($fp = @fsockopen('localhost', 80, $errorCode, $errorMessage, 2))) {
72+
$this->markTestSkipped('FrankenPHP is not running');
73+
}
74+
fclose($fp);
75+
76+
$p = new Process(['curl', '-v', 'http://localhost/early_hints.php']);
77+
$p->run();
78+
$output = $p->getErrorOutput();
79+
80+
$this->assertSame(3, preg_match_all('#Link: </css/style\.css>; rel="preload"; as="style"#', $output));
81+
$this->assertSame(2, preg_match_all('#Link: </js/app\.js>; rel="preload"; as="script"#', $output));
82+
}
5783
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy