Skip to content

Commit a3a35df

Browse files
authored
Merge pull request #7445 from kenjis/fix-redirect-status-code
fix: redirect status code
2 parents 3f0a198 + 1c3a3b7 commit a3a35df

File tree

13 files changed

+246
-70
lines changed

13 files changed

+246
-70
lines changed

system/HTTP/RedirectResponse.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public function to(string $uri, ?int $code = null, string $method = 'auto')
5050
*
5151
* @throws HTTPException
5252
*/
53-
public function route(string $route, array $params = [], int $code = 302, string $method = 'auto')
53+
public function route(string $route, array $params = [], ?int $code = null, string $method = 'auto')
5454
{
5555
$namedRoute = $route;
5656

system/HTTP/ResponseTrait.php

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -494,29 +494,41 @@ public function sendBody()
494494
/**
495495
* Perform a redirect to a new URL, in two flavors: header or location.
496496
*
497-
* @param string $uri The URI to redirect to
498-
* @param int $code The type of redirection, defaults to 302
497+
* @param string $uri The URI to redirect to
498+
* @param int|null $code The type of redirection, defaults to 302
499499
*
500500
* @return $this
501501
*
502502
* @throws HTTPException For invalid status code.
503503
*/
504504
public function redirect(string $uri, string $method = 'auto', ?int $code = null)
505505
{
506-
// Assume 302 status code response; override if needed
507-
if (empty($code)) {
508-
$code = 302;
509-
}
510-
511506
// IIS environment likely? Use 'refresh' for better compatibility
512-
if ($method === 'auto' && isset($_SERVER['SERVER_SOFTWARE']) && strpos($_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS') !== false) {
507+
if (
508+
$method === 'auto'
509+
&& isset($_SERVER['SERVER_SOFTWARE'])
510+
&& strpos($_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS') !== false
511+
) {
513512
$method = 'refresh';
513+
} elseif ($method !== 'refresh' && $code === null) {
514+
// override status code for HTTP/1.1 & higher
515+
if (
516+
isset($_SERVER['SERVER_PROTOCOL'], $_SERVER['REQUEST_METHOD'])
517+
&& $this->getProtocolVersion() >= 1.1
518+
) {
519+
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
520+
$code = 302;
521+
} elseif (in_array($_SERVER['REQUEST_METHOD'], ['POST', 'PUT', 'DELETE'], true)) {
522+
// reference: https://en.wikipedia.org/wiki/Post/Redirect/Get
523+
$code = 303;
524+
} else {
525+
$code = 307;
526+
}
527+
}
514528
}
515529

516-
// override status code for HTTP/1.1 & higher
517-
// reference: http://en.wikipedia.org/wiki/Post/Redirect/Get
518-
if (isset($_SERVER['SERVER_PROTOCOL'], $_SERVER['REQUEST_METHOD']) && $this->getProtocolVersion() >= 1.1 && $method !== 'refresh') {
519-
$code = ($_SERVER['REQUEST_METHOD'] !== 'GET') ? 303 : ($code === 302 ? 307 : $code);
530+
if ($code === null) {
531+
$code = 302;
520532
}
521533

522534
switch ($method) {

tests/system/CodeIgniterTest.php

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -446,15 +446,18 @@ public function testRunRedirectionWithURI()
446446
/**
447447
* @see https://github.com/codeigniter4/CodeIgniter4/issues/3041
448448
*/
449-
public function testRunRedirectionWithURINotSet()
449+
public function testRunRedirectionWithGET()
450450
{
451451
$_SERVER['argv'] = ['index.php', 'example'];
452452
$_SERVER['argc'] = 2;
453453

454-
$_SERVER['REQUEST_URI'] = '/example';
454+
$_SERVER['REQUEST_URI'] = '/example';
455+
$_SERVER['SERVER_PROTOCOL'] = 'HTTP/1.1';
456+
$_SERVER['REQUEST_METHOD'] = 'GET';
455457

456458
// Inject mock router.
457459
$routes = Services::routes();
460+
// addRedirect() sets status code 302 by default.
458461
$routes->addRedirect('example', 'pages/notset');
459462

460463
$router = Services::router($routes, Services::incomingrequest());
@@ -463,11 +466,37 @@ public function testRunRedirectionWithURINotSet()
463466
ob_start();
464467
$this->codeigniter->run();
465468
ob_get_clean();
469+
466470
$response = $this->getPrivateProperty($this->codeigniter, 'response');
467471
$this->assertSame('http://example.com/pages/notset', $response->header('Location')->getValue());
472+
$this->assertSame(302, $response->getStatusCode());
473+
}
474+
475+
public function testRunRedirectionWithGETAndHTTPCode301()
476+
{
477+
$_SERVER['argv'] = ['index.php', 'example'];
478+
$_SERVER['argc'] = 2;
479+
480+
$_SERVER['REQUEST_URI'] = '/example';
481+
$_SERVER['SERVER_PROTOCOL'] = 'HTTP/1.1';
482+
$_SERVER['REQUEST_METHOD'] = 'GET';
483+
484+
// Inject mock router.
485+
$routes = Services::routes();
486+
$routes->addRedirect('example', 'pages/notset', 301);
487+
488+
$router = Services::router($routes, Services::incomingrequest());
489+
Services::injectMock('router', $router);
490+
491+
ob_start();
492+
$this->codeigniter->run();
493+
ob_get_clean();
494+
495+
$response = $this->getPrivateProperty($this->codeigniter, 'response');
496+
$this->assertSame(301, $response->getStatusCode());
468497
}
469498

470-
public function testRunRedirectionWithHTTPCode303()
499+
public function testRunRedirectionWithPOSTAndHTTPCode301()
471500
{
472501
$_SERVER['argv'] = ['index.php', 'example'];
473502
$_SERVER['argc'] = 2;
@@ -488,7 +517,7 @@ public function testRunRedirectionWithHTTPCode303()
488517
ob_get_clean();
489518

490519
$response = $this->getPrivateProperty($this->codeigniter, 'response');
491-
$this->assertSame(303, $response->getStatusCode());
520+
$this->assertSame(301, $response->getStatusCode());
492521
}
493522

494523
public function testStoresPreviousURL()

tests/system/HTTP/ResponseTest.php

Lines changed: 73 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -269,23 +269,84 @@ public function testRedirectSetsDefaultCodeAndLocationHeader()
269269
$this->assertSame(302, $response->getStatusCode());
270270
}
271271

272-
public function testRedirectSetsCode()
273-
{
274-
$response = new Response(new App());
272+
/**
273+
* @dataProvider provideForRedirect
274+
*/
275+
public function testRedirect(
276+
string $server,
277+
string $protocol,
278+
string $method,
279+
?int $code,
280+
int $expectedCode
281+
) {
282+
$_SERVER['SERVER_SOFTWARE'] = $server;
283+
$_SERVER['SERVER_PROTOCOL'] = $protocol;
284+
$_SERVER['REQUEST_METHOD'] = $method;
275285

276-
$response->redirect('example.com', 'auto', 307);
286+
$response = new Response(new App());
287+
$response->redirect('example.com', 'auto', $code);
277288

278289
$this->assertTrue($response->hasHeader('location'));
279290
$this->assertSame('example.com', $response->getHeaderLine('Location'));
280-
$this->assertSame(307, $response->getStatusCode());
291+
$this->assertSame($expectedCode, $response->getStatusCode());
292+
}
293+
294+
public function provideForRedirect()
295+
{
296+
yield from [
297+
['Apache/2.4.17', 'HTTP/1.1', 'GET', null, 302],
298+
['Apache/2.4.17', 'HTTP/1.1', 'GET', 307, 307],
299+
['Apache/2.4.17', 'HTTP/1.1', 'GET', 302, 302],
300+
['Apache/2.4.17', 'HTTP/1.1', 'POST', null, 303],
301+
['Apache/2.4.17', 'HTTP/1.1', 'POST', 307, 307],
302+
['Apache/2.4.17', 'HTTP/1.1', 'POST', 302, 302],
303+
['Apache/2.4.17', 'HTTP/1.1', 'HEAD', null, 307],
304+
['Apache/2.4.17', 'HTTP/1.1', 'HEAD', 307, 307],
305+
['Apache/2.4.17', 'HTTP/1.1', 'HEAD', 302, 302],
306+
['Apache/2.4.17', 'HTTP/1.1', 'OPTIONS', null, 307],
307+
['Apache/2.4.17', 'HTTP/1.1', 'OPTIONS', 307, 307],
308+
['Apache/2.4.17', 'HTTP/1.1', 'OPTIONS', 302, 302],
309+
['Apache/2.4.17', 'HTTP/1.1', 'PUT', null, 303],
310+
['Apache/2.4.17', 'HTTP/1.1', 'PUT', 307, 307],
311+
['Apache/2.4.17', 'HTTP/1.1', 'PUT', 302, 302],
312+
['Apache/2.4.17', 'HTTP/1.1', 'DELETE', null, 303],
313+
['Apache/2.4.17', 'HTTP/1.1', 'DELETE', 307, 307],
314+
['Apache/2.4.17', 'HTTP/1.1', 'DELETE', 302, 302],
315+
];
281316
}
282317

283-
public function testRedirectWithIIS()
284-
{
318+
/**
319+
* @dataProvider provideForRedirectWithIIS
320+
*/
321+
public function testRedirectWithIIS(
322+
string $protocol,
323+
string $method,
324+
?int $code,
325+
int $expectedCode
326+
) {
285327
$_SERVER['SERVER_SOFTWARE'] = 'Microsoft-IIS';
286-
$response = new Response(new App());
287-
$response->redirect('example.com', 'auto', 307);
328+
$_SERVER['SERVER_PROTOCOL'] = 'HTTP/1.1';
329+
$_SERVER['REQUEST_METHOD'] = 'POST';
330+
331+
$response = new Response(new App());
332+
$response->redirect('example.com', 'auto', $code);
333+
288334
$this->assertSame('0;url=example.com', $response->getHeaderLine('Refresh'));
335+
$this->assertSame($expectedCode, $response->getStatusCode());
336+
337+
unset($_SERVER['SERVER_SOFTWARE']);
338+
}
339+
340+
public function provideForRedirectWithIIS()
341+
{
342+
yield from [
343+
['HTTP/1.1', 'GET', null, 302],
344+
['HTTP/1.1', 'GET', 307, 307],
345+
['HTTP/1.1', 'GET', 302, 302],
346+
['HTTP/1.1', 'POST', null, 302],
347+
['HTTP/1.1', 'POST', 307, 307],
348+
['HTTP/1.1', 'POST', 302, 302],
349+
];
289350
}
290351

291352
public function testSetCookieFails()
@@ -458,7 +519,7 @@ public function testMisbehaving()
458519
$response->getStatusCode();
459520
}
460521

461-
public function testTemporaryRedirect11()
522+
public function testTemporaryRedirectHTTP11()
462523
{
463524
$_SERVER['SERVER_PROTOCOL'] = 'HTTP/1.1';
464525
$_SERVER['REQUEST_METHOD'] = 'POST';
@@ -470,7 +531,7 @@ public function testTemporaryRedirect11()
470531
$this->assertSame(303, $response->getStatusCode());
471532
}
472533

473-
public function testTemporaryRedirectGet11()
534+
public function testTemporaryRedirectGetHTTP11()
474535
{
475536
$_SERVER['SERVER_PROTOCOL'] = 'HTTP/1.1';
476537
$_SERVER['REQUEST_METHOD'] = 'GET';
@@ -479,7 +540,7 @@ public function testTemporaryRedirectGet11()
479540
$response->setProtocolVersion('HTTP/1.1');
480541
$response->redirect('/foo');
481542

482-
$this->assertSame(307, $response->getStatusCode());
543+
$this->assertSame(302, $response->getStatusCode());
483544
}
484545

485546
// Make sure cookies are set by RedirectResponse this way

user_guide_src/source/changelogs/v4.3.4.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,29 @@ Release Date: Unreleased
1212
BREAKING
1313
********
1414

15+
Behavior Changes
16+
================
17+
18+
.. _v434-redirect-status-code:
19+
20+
Redirect Status Code
21+
--------------------
22+
23+
- Due to a bug, in previous versions, when using HTTP/1.1 or later the status
24+
code of the actual redirect response might be changed even if a status code was
25+
specified. For example, for a GET request, 302 would change to 307; for a POST
26+
request, 307 and 302 would change to 303.
27+
- Starting with this version, if you specify a status code in
28+
:ref:`redirect <response-redirect-status-code>`, that code will always be used
29+
in the response.
30+
- The default code for GET requests has been corrected from 307 to 302 when using
31+
HTTP/1.1 or later.
32+
- The default code for HEAD and OPTIONS requests has been corrected from 303 to
33+
307 when using HTTP/1.1 or later.
34+
- In ``$routes->addRedirect()``, 302 is specified by default. Therefor 302 will
35+
always be used when you don't specify a status code. In previous versions,
36+
302 might be changed.
37+
1538
Message Changes
1639
***************
1740

user_guide_src/source/general/common_functions.rst

Lines changed: 1 addition & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -320,48 +320,8 @@ Miscellaneous Functions
320320
:param string $route: The route name or Controller::method to redirect the user to.
321321
:rtype: RedirectResponse
322322

323-
.. important:: When you use this function, an instance of ``RedirectResponse`` must be returned
324-
in the method of the :doc:`Controller <../incoming/controllers>` or
325-
the :doc:`Controller Filter <../incoming/filters>`. If you forget to return it,
326-
no redirection will occur.
327-
328323
Returns a RedirectResponse instance allowing you to easily create redirects.
329-
330-
**Redirect to a URI path**
331-
332-
When you want to pass a URI path (relative to baseURL), use ``redirect()->to()``:
333-
334-
.. literalinclude:: common_functions/005.php
335-
:lines: 2-
336-
337-
.. note:: If there is a fragment in your URL that you want to remove, you can use the refresh parameter in this function.
338-
Like ``return redirect()->to('to', null, 'refresh');``.
339-
340-
**Redirect to a Defined Route**
341-
342-
When you want to pass a :ref:`route name <using-named-routes>` or Controller::method
343-
for :ref:`reverse routing <reverse-routing>`, use ``redirect()->route()``:
344-
345-
.. literalinclude:: common_functions/013.php
346-
:lines: 2-
347-
348-
When passing an argument into the function, it is treated as a route name or
349-
Controller::method for reverse routing, not a relative/full URI,
350-
treating it the same as using ``redirect()->route()``:
351-
352-
.. literalinclude:: common_functions/006.php
353-
:lines: 2-
354-
355-
**Redirect Back**
356-
357-
When you want to redirect back, use ``redirect()->back()``:
358-
359-
.. literalinclude:: common_functions/014.php
360-
:lines: 2-
361-
362-
.. note:: ``redirect()->back()`` is not the same as browser "back" button.
363-
It takes a visitor to "the last page viewed during the Session" when the Session is available.
364-
If the Session hasn’t been loaded, or is otherwise unavailable, then a sanitized version of HTTP_REFERER will be used.
324+
See :ref:`response-redirect` for details.
365325

366326
.. php:function:: remove_invisible_characters($str[, $urlEncoded = true])
367327

user_guide_src/source/installation/upgrade_434.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@ Mandatory File Changes
1818
Breaking Changes
1919
****************
2020

21+
Redirect Status Code
22+
====================
23+
24+
- Due to a bug fix, the status codes of redirects may be changed. See
25+
:ref:`ChangeLog v4.3.4 <v434-redirect-status-code>` and if the code is not
26+
what you want, :ref:`specify status codes <response-redirect-status-code>`.
27+
2128
Breaking Enhancements
2229
*********************
2330

0 commit comments

Comments
 (0)