diff --git a/UPGRADE-6.3.md b/UPGRADE-6.3.md index d9277955dc642..1c26cc3541600 100644 --- a/UPGRADE-6.3.md +++ b/UPGRADE-6.3.md @@ -4,4 +4,4 @@ UPGRADE FROM 6.2 to 6.3 HttpFoundation -------------- - * Deprecate calling `Response::sendHeaders()` without any arguments + * `Response::sendHeaders()` now takes an optional `$statusCode` parameter diff --git a/src/Symfony/Component/HttpFoundation/Response.php b/src/Symfony/Component/HttpFoundation/Response.php index e4e474f660118..3b48b55ba611d 100644 --- a/src/Symfony/Component/HttpFoundation/Response.php +++ b/src/Symfony/Component/HttpFoundation/Response.php @@ -110,6 +110,11 @@ class Response */ public $headers; + /** + * Track + */ + private ?ResponseHeaderBag $alreadySentHeaders = null; + /** * @var string */ @@ -211,6 +216,13 @@ class Response 511 => 'Network Authentication Required', // RFC6585 ]; + /** + * @var array + * + * Tracks headers already sent in informational responses. + */ + private array $sentHeaders; + /** * @param int $status The HTTP status code (200 "OK" by default) * @@ -332,25 +344,36 @@ public function prepare(Request $request): static */ public function sendHeaders(/* ?int $statusCode = null */): 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; } + $statusCode = \func_num_args() > 0 ? func_get_arg(0) : null; + $informationalResponse = $statusCode >= 100 && $statusCode < 200; + if ($informationalResponse && !\function_exists('headers_send')) { + // skip informational responses if not supported by the SAPI + return $this; + } + // headers foreach ($this->headers->allPreserveCaseWithoutCookies() as $name => $values) { + $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'); - foreach ($values as $value) { + + $newValues = null === $previousValues ? $values : array_diff($values, $previousValues); + foreach ($newValues as $value) { header($name.': '.$value, $replace, $this->statusCode); } + + if ($informationalResponse) { + $this->sentHeaders[$name] = $values; + } } // cookies @@ -358,21 +381,14 @@ public function sendHeaders(/* ?int $statusCode = null */): static header('Set-Cookie: '.$cookie, false, $this->statusCode); } - if ($statusCode) { - if (\function_exists('headers_send')) { - headers_send($statusCode); + if ($informationalResponse) { + 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; + return $this; } + $statusCode ??= $this->statusCode; + // status header(sprintf('HTTP/%s %s %s', $this->version, $statusCode, $this->statusText), true, $statusCode); diff --git a/src/Symfony/Component/HttpFoundation/StreamedResponse.php b/src/Symfony/Component/HttpFoundation/StreamedResponse.php index e1064e2f1e142..6db476b08bc34 100644 --- a/src/Symfony/Component/HttpFoundation/StreamedResponse.php +++ b/src/Symfony/Component/HttpFoundation/StreamedResponse.php @@ -63,7 +63,7 @@ public function setCallback(callable $callback): static * * @return $this */ - public function sendHeaders(): static + public function sendHeaders(/* ?int $statusCode = null */): static { if ($this->headersSent) { return $this;