Skip to content

Commit 22b8e9e

Browse files
fix: prioritize headers set by the Response class (codeigniter4#9235)
* fix: prioritize headers set by the Response class * cs fix * add a note to the user guide * docs: add an example to the header changes * Update user_guide_src/source/changelogs/v4.6.0.rst Co-authored-by: John Paul E. Balandan, CPA <[email protected]> --------- Co-authored-by: John Paul E. Balandan, CPA <[email protected]>
1 parent 345e1c3 commit 22b8e9e

File tree

4 files changed

+105
-2
lines changed

4 files changed

+105
-2
lines changed

system/HTTP/ResponseTrait.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -403,16 +403,19 @@ public function sendHeaders()
403403
if ($value instanceof Header) {
404404
header(
405405
$name . ': ' . $value->getValueLine(),
406-
false,
406+
true,
407407
$this->getStatusCode()
408408
);
409409
} else {
410+
$replace = true;
411+
410412
foreach ($value as $header) {
411413
header(
412414
$name . ': ' . $header->getValueLine(),
413-
false,
415+
$replace,
414416
$this->getStatusCode()
415417
);
418+
$replace = false;
416419
}
417420
}
418421
}

tests/system/HTTP/ResponseSendTest.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,4 +179,54 @@ public function testDoNotSendUnSecureCookie(): void
179179
// send it
180180
$response->send();
181181
}
182+
183+
/**
184+
* Make sure that the headers set by the header() function
185+
* are overridden by the headers defined in the Response class.
186+
*/
187+
#[PreserveGlobalState(false)]
188+
#[RunInSeparateProcess]
189+
#[WithoutErrorHandler]
190+
public function testHeaderOverride(): void
191+
{
192+
$response = new Response(new App());
193+
$response->pretend(false);
194+
195+
$body = 'Hello';
196+
$response->setBody($body);
197+
198+
// single header
199+
$response->setHeader('Vary', 'Accept-Encoding');
200+
$this->assertSame('Accept-Encoding', $response->header('Vary')->getValue());
201+
202+
// multiple headers
203+
$response->setHeader('Access-Control-Expose-Headers', 'X-Custom-Header');
204+
$response->addHeader('Access-Control-Expose-Headers', 'Content-Length');
205+
$header = $response->header('Access-Control-Expose-Headers');
206+
$this->assertIsArray($header);
207+
$this->assertSame('X-Custom-Header', $header[0]->getValue());
208+
$this->assertSame('Content-Length', $header[1]->getValue());
209+
210+
// send it
211+
ob_start();
212+
header('Vary: User-Agent');
213+
header('Access-Control-Expose-Headers: Content-Encoding');
214+
header('Allow: GET, POST');
215+
$response->send();
216+
if (ob_get_level() > 0) {
217+
ob_end_clean();
218+
}
219+
220+
// single header
221+
$this->assertHeaderEmitted('Vary: Accept-Encoding');
222+
$this->assertHeaderNotEmitted('Vary: User-Agent');
223+
224+
// multiple headers
225+
$this->assertHeaderEmitted('Access-Control-Expose-Headers: X-Custom-Header');
226+
$this->assertHeaderEmitted('Access-Control-Expose-Headers: Content-Length');
227+
$this->assertHeaderNotEmitted('Access-Control-Expose-Headers: Content-Encoding');
228+
229+
// not overridden by the response class
230+
$this->assertHeaderEmitted('Allow: GET, POST');
231+
}
182232
}

user_guide_src/source/changelogs/v4.6.0.rst

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,48 @@ See :ref:`Upgrading Guide <upgrade-460-sid-change>` for details.
9999

100100
.. _v460-interface-changes:
101101

102+
Headers
103+
-------
104+
105+
The headers set by the ``Response`` class replace those that can be set by the PHP
106+
``header()`` function.
107+
108+
In previous versions, headers set by the ``Response`` class were added to existing
109+
ones - giving no options to change them. That could lead to unexpected behavior when
110+
the same headers were set with mutually exclusive directives.
111+
112+
For example, session will automatically set headers with the ``header()`` function:
113+
114+
.. code-block:: none
115+
116+
Expires: Thu, 19 Nov 1981 08:52:00 GMT
117+
Cache-Control: no-store, no-cache, must-revalidate
118+
Pragma: no-cache
119+
120+
So if we set **Expires** header one more time we will end up with a duplicated header:
121+
122+
.. code-block:: php
123+
124+
$response->removeHeader('Expires'); // has no effect
125+
return $response->setHeader('Expires', 'Sun, 17 Nov 2024 14:17:37 GMT');
126+
127+
Response headers:
128+
129+
.. code-block:: none
130+
131+
Expires: Thu, 19 Nov 1981 08:52:00 GMT
132+
// ...
133+
Expires: Sun, 17 Nov 2024 14:17:37 GMT
134+
135+
Now, we don't know which one will be picked by the browser or which header is the correct one.
136+
With changes in this version our previous header will be overridden:
137+
138+
.. code-block:: none
139+
140+
Cache-Control: no-store, no-cache, must-revalidate
141+
Pragma: no-cache
142+
Expires: Sun, 17 Nov 2024 14:17:37 GMT
143+
102144
Interface Changes
103145
=================
104146

@@ -298,6 +340,10 @@ Deprecations
298340
Bugs Fixed
299341
**********
300342

343+
- **Response:**
344+
- Headers set using the ``Response`` class are now prioritized and replace headers
345+
that can be set manually using the PHP ``header()`` function.
346+
301347
See the repo's
302348
`CHANGELOG.md <https://github.com/codeigniter4/CodeIgniter4/blob/develop/CHANGELOG.md>`_
303349
for a complete list of bugs fixed.

user_guide_src/source/outgoing/response.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ which can be either a string or an array of values that will be combined correct
5757
Using these functions instead of using the native PHP functions allows you to ensure that no headers are sent
5858
prematurely, causing errors, and makes testing possible.
5959

60+
.. important:: Since v4.6.0, if you set a header using PHP's native ``header()``
61+
function and then use the ``Response`` class to set the same header, the
62+
previous one will be overwritten.
63+
6064
.. note:: This method just sets headers to the response instance. So, if you create
6165
and return another response instance (e.g., if you call :php:func:`redirect()`),
6266
the headers set here will not be sent automatically.

0 commit comments

Comments
 (0)