Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Environment-independent 301/302 redirection with PHP

I've run across accidental usage of Status header for FastCGI. Are there pros/cons of using it in environment-independent scripts?

header('Location: ' . $url, true, 301);

alone causes no issues for me on Apache 2.2 (according to phpinfo(), the server uses FastCGI).

The script is aimed at Apache and nginx (mod_php and FastCGI). What would fail-proof solution look like?

like image 445
Estus Flask Avatar asked Aug 10 '15 17:08

Estus Flask


1 Answers

HTTP Status code is emitted as part of the first line of the HTTP response. According to Fast CGI FAQ the Status header is a special header recognized by the server which controls this line and it is not sent to the client. However if it is used with a non FastCGI server adapter, the value is ignored by the server and the header can be sent out.

The solution you already have is the most environment-independent way possible. The only addition would be an exit statement immediately after redirect to make sure the script terminates.

Let's look what happens under the hood more closely.

The following PHP redirection code

header('Location: ' . $url, true, 301);
exit;

Will call the C code in ext/standard/head.c

PHP_FUNCTION(header)
{
    [ code that just parses the arguments omitted ]

    sapi_header_op(rep ? SAPI_HEADER_REPLACE:SAPI_HEADER_ADD, &ctr);
}

Which will in turn call the sapi_header_op function in main/SAPI.c

[ ... ]

switch (op) {

    [ ... ]

    case SAPI_HEADER_ADD:
    case SAPI_HEADER_REPLACE:
    case SAPI_HEADER_DELETE: {
            sapi_header_line *p = arg;

            if (!p->line || !p->line_len) {
                return FAILURE;
            }
            header_line = p->line;
            header_line_len = p->line_len;
            http_response_code = p->response_code;
            break;
        }

[ code that splits header line by colon, trims whitespace etc ]

[ special headers handling code, including setting 302 if Location  ]

if (http_response_code) {
    sapi_update_response_code(http_response_code);
}

sapi_header_add_op(op, &sapi_header);
return SUCCESS;

If FastCGI back-end is in use, the added headers will eventually be sent out by sapi_cgi_send_headers function in sapi/cgi/cgi_main.c

[ ... ]
if (CGIG(nph) || SG(sapi_headers).http_response_code != 200)
{
    [ emit status line if cgi.rfc2616-headers is set ]

    [ Handle a case where there is a user supplied status line ]

    [ Handle a case where there is already a user supplied status header ]

    [ if none of the above ]

        if (err->str) {
            len = slprintf(buf, sizeof(buf), "Status: %d %s\r\n", SG(sapi_headers).http_response_code, err->str);
        } else {
            len = slprintf(buf, sizeof(buf), "Status: %d\r\n", SG(sapi_headers).http_response_code);
        }
     [ ... ]
}
[ ... ]

Note that php_apache_sapi_send_headers function in sapi/apache2handler/sapi_apache2.c does not have any special handling of the Status header because it is not used for module communication.

So by executing the PHP code above

  1. Response code in the HTTP status line is forced to 301
  2. Location header is either added or the existing one is replaced
  3. The script exits so no subsequent code can change the status or headers

All manipulations are performed in the SAPI layer which is an abstraction layer on top of HTTP server adapters (FastCGI, Apache module etc). This is as cross-environment and reliable as it gets.

Historically there have been bugs in FastCGI which prevented 301 responses from working correctly, however these were in the web server implementation and there was nothing that could of been done from PHP code to work around the issue.

See also:

  • https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Response_message
  • https://github.com/php/php-src/blob/a8d38674b1d75de54bb1d3d1436da759ef8e64d7/ext/standard/head.c
  • https://github.com/php/php-src/blob/ebb6f5eae6b37b0202eff325b05932f0b1c28944/main/SAPI.c#L841
  • https://github.com/php/php-src/blob/ebb6f5eae6b37b0202eff325b05932f0b1c28944/sapi/cgi/cgi_main.c#L405
  • https://github.com/php/php-src/blob/ebb6f5eae6b37b0202eff325b05932f0b1c28944/sapi/apache2handler/sapi_apache2.c#L151
  • php 301 redirects actually doing a 302 redirect
like image 136
anttix Avatar answered Oct 05 '22 07:10

anttix