From 85bb5ca6befc32e545ea80d3ee73ddd01ba5ea09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Sun, 6 Nov 2022 21:23:24 +0100 Subject: [PATCH] [HttpFoundation] Add support for the 103 status code (Early Hints) --- UPGRADE-6.3.md | 7 +++++ .../Component/HttpFoundation/CHANGELOG.md | 5 ++++ .../Component/HttpFoundation/Response.php | 29 +++++++++++++++++-- .../HttpFoundation/StreamedResponse.php | 4 ++- .../response-functional/deleted_cookie.php | 2 +- .../HttpFoundation/Tests/ResponseTest.php | 13 ++++++++- .../Tests/StreamedResponseTest.php | 4 +-- .../Component/HttpKernel/HttpKernel.php | 2 +- 8 files changed, 58 insertions(+), 8 deletions(-) create mode 100644 UPGRADE-6.3.md diff --git a/UPGRADE-6.3.md b/UPGRADE-6.3.md new file mode 100644 index 0000000000000..d9277955dc642 --- /dev/null +++ b/UPGRADE-6.3.md @@ -0,0 +1,7 @@ +UPGRADE FROM 6.2 to 6.3 +======================= + +HttpFoundation +-------------- + + * Deprecate calling `Response::sendHeaders()` without any arguments diff --git a/src/Symfony/Component/HttpFoundation/CHANGELOG.md b/src/Symfony/Component/HttpFoundation/CHANGELOG.md index 790767d728190..810ef07f1f85e 100644 --- a/src/Symfony/Component/HttpFoundation/CHANGELOG.md +++ b/src/Symfony/Component/HttpFoundation/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +6.3 +--- + + * The `Response::sendHeaders()` method now takes an HTTP status code as parameter, allowing to send informational responses such as Early Hints responses (103 status code) + 6.2 --- diff --git a/src/Symfony/Component/HttpFoundation/Response.php b/src/Symfony/Component/HttpFoundation/Response.php index d5c7129a5fa5a..fd27155eb77b5 100644 --- a/src/Symfony/Component/HttpFoundation/Response.php +++ b/src/Symfony/Component/HttpFoundation/Response.php @@ -326,10 +326,20 @@ public function prepare(Request $request): static /** * Sends HTTP headers. * + * @param null|positive-int $statusCode The status code to use. Override the statusCode property if set and not null. + * * @return $this */ public function sendHeaders(): static { + if (1 > \func_num_args()) { + trigger_deprecation('symfony/http-foundation', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + + $statusCode = null; + } else { + $statusCode = func_get_arg(0) ?: null; + } + // headers have already been sent by the developer if (headers_sent()) { return $this; @@ -348,8 +358,23 @@ public function sendHeaders(): static header('Set-Cookie: '.$cookie, false, $this->statusCode); } + if ($statusCode) { + if (\function_exists('headers_send')) { + headers_send($statusCode); + + return $this; + } + + if ($statusCode >= 100 && $statusCode < 200) { + // skip informational responses if not supported by the SAPI + return $this; + } + } else { + $statusCode = $this->statusCode; + } + // status - header(sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText), true, $this->statusCode); + header(sprintf('HTTP/%s %s %s', $this->version, $statusCode, $this->statusText), true, $statusCode); return $this; } @@ -373,7 +398,7 @@ public function sendContent(): static */ public function send(): static { - $this->sendHeaders(); + $this->sendHeaders(null); $this->sendContent(); if (\function_exists('fastcgi_finish_request')) { diff --git a/src/Symfony/Component/HttpFoundation/StreamedResponse.php b/src/Symfony/Component/HttpFoundation/StreamedResponse.php index 0bddcdc9bb731..e1064e2f1e142 100644 --- a/src/Symfony/Component/HttpFoundation/StreamedResponse.php +++ b/src/Symfony/Component/HttpFoundation/StreamedResponse.php @@ -59,6 +59,8 @@ public function setCallback(callable $callback): static /** * This method only sends the headers once. * + * @param null|positive-int $statusCode The status code to use. Override the statusCode property if set and not null. + * * @return $this */ public function sendHeaders(): static @@ -69,7 +71,7 @@ public function sendHeaders(): static $this->headersSent = true; - return parent::sendHeaders(); + return parent::sendHeaders(...\func_get_args()); } /** diff --git a/src/Symfony/Component/HttpFoundation/Tests/Fixtures/response-functional/deleted_cookie.php b/src/Symfony/Component/HttpFoundation/Tests/Fixtures/response-functional/deleted_cookie.php index 003b0c121f888..dd156b46fb3ae 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/Fixtures/response-functional/deleted_cookie.php +++ b/src/Symfony/Component/HttpFoundation/Tests/Fixtures/response-functional/deleted_cookie.php @@ -57,4 +57,4 @@ public function handle(Request $request, int $type = self::MAIN_REQUEST, bool $c $listener->onKernelResponse(new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $r)); -$r->sendHeaders(); +$r->sendHeaders(null); diff --git a/src/Symfony/Component/HttpFoundation/Tests/ResponseTest.php b/src/Symfony/Component/HttpFoundation/Tests/ResponseTest.php index 90734a8cffbc2..68281c91c6952 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/ResponseTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/ResponseTest.php @@ -38,7 +38,7 @@ public function testClone() public function testSendHeaders() { $response = new Response(); - $headers = $response->sendHeaders(); + $headers = $response->sendHeaders(null); $this->assertObjectHasAttribute('headers', $headers); $this->assertObjectHasAttribute('content', $headers); $this->assertObjectHasAttribute('version', $headers); @@ -47,6 +47,17 @@ public function testSendHeaders() $this->assertObjectHasAttribute('charset', $headers); } + public function testSendInformationalResponse() + { + $response = new Response(); + $response->sendHeaders(103); + + // Informational responses must not override the main status code + $this->assertSame(200, $response->getStatusCode()); + + $response->sendHeaders(null); + } + public function testSend() { $response = new Response(); diff --git a/src/Symfony/Component/HttpFoundation/Tests/StreamedResponseTest.php b/src/Symfony/Component/HttpFoundation/Tests/StreamedResponseTest.php index 3ebae422b75dc..45566e8613b02 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/StreamedResponseTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/StreamedResponseTest.php @@ -108,8 +108,8 @@ public function testReturnThis() $this->assertInstanceOf(StreamedResponse::class, $response->sendContent()); $response = new StreamedResponse(function () {}); - $this->assertInstanceOf(StreamedResponse::class, $response->sendHeaders()); - $this->assertInstanceOf(StreamedResponse::class, $response->sendHeaders()); + $this->assertInstanceOf(StreamedResponse::class, $response->sendHeaders(null)); + $this->assertInstanceOf(StreamedResponse::class, $response->sendHeaders(null)); } public function testSetNotModified() diff --git a/src/Symfony/Component/HttpKernel/HttpKernel.php b/src/Symfony/Component/HttpKernel/HttpKernel.php index 7f5c592506c0f..eb1cd87b81020 100644 --- a/src/Symfony/Component/HttpKernel/HttpKernel.php +++ b/src/Symfony/Component/HttpKernel/HttpKernel.php @@ -118,7 +118,7 @@ public function terminateWithException(\Throwable $exception, Request $request = } } - $response->sendHeaders(); + $response->sendHeaders(null); $response->sendContent(); $this->terminate($request, $response);