How to change HTTP response and show an error mess

2019-08-10 23:07发布

问题:

I have a situation where a PHP function attempts to redirect the browser via an HTTP 302, but an exception is being thrown in a destructor called upon 'exit'ing. The actual code in question is the _doRedirect() method of SimpleSAML but here's a simplified situation:

header('Location: http://somewhere.com', TRUE, 302);
exit; // end script execution

The 'exit' is triggering a destructor for an unrelated class and the error details are getting written out to the HTTP response... but no human notices the error because the browser executes the HTTP 302 redirect.

Ideally I'd like to change the status code to HTTP 500 so the browser would just render the page with the error on it. But is that possible? If not, what options do I have?

回答1:

Unfortunately, you are meeting a by-design issue.

  • The set_error_handler function does not handle uncaught exceptions (which is a level E_ERROR error):

From the documentation:

The following error types cannot be handled with a user defined function: E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_ERROR, E_COMPILE_WARNING, and most of E_STRICT raised in the file where set_error_handler() is called.

  • The reguster_shutdown_function function is called after your exit call (or when the end of the script is reached), but before your objects destruction.

So if you are trying such code:

<?php

register_shutdown_function(function() {
  $error = error_get_last();
  if (!is_null($error)) {
    header_remove();
    header("HTTP/1.1 500 Internal Server Error");
    // do something with $error
  }
  else {
    echo "no error happened";
  }
});

class A
{
   public function __destruct()
   {
     throw new \Exception("Hey dude!");
   }
}

header("HTTP/1.1 301 Moved Permanently");
$a = new A;

You'll get "no error happened" and the fatal error displayed, and the 301 will remain unchanged.

If you think you may destroy your object from inside the register_shutdown_function callback, you'll meet a scoping issue where your object is referenced in its original scope, and in the register_shutdown_function's scope: as one reference of the object still exist, it will not be destroyed.

<?php

class A
{
   public function __destruct()
   {
     throw new \Exception("Hey dude!");
   }
}

$a = new A; /* global scope */

register_shutdown_function(function() use ($a /* local scope */) {
  try {
    unset($a); /* removed from local scope but another reference still exists in global scope */
  } catch (\Exception $e) {
    echo "Catched!\n";
  }
});

/* destructor called here */

Even well-known debug components do not support this.

For example using the Symfony2 Debug component:

composer.json

{
  "require": {
    "symfony/debug": "~2.5"
  }
}

test.php

<?php

require('vendor/autoload.php');

use Symfony\Component\Debug\Debug;

Debug::enable();

class A
{
   public function __destruct()
   {
     throw new \Exception("Hey dude!");
   }
}

$a = new A;

The component is enabled but does not display the nice expected exception.



回答2:

I think the sequence of events is as follows:

/* 1 */ header("Location: /some/path");
/* 2 */ echo "Some content";
/* 3 */ exit;
/* 3.1 */ throw new Exception("Some exception");

When content is sent in step #2, PHP has to send the header as well which is set to 302. Once the header is sent, an exception cannot change the status code. The error message generated is ignored by the browser as it navigates away to the specified location.

Solution:

You can try adding ob_start() to start buffering at a suitable location. By suitable location I mean anywhere before the step 1 in the above illustration. Buffering defers the sending of header and/or content until page is processed (or buffer is flushed explicitly). This might give PHP a chance to alter response code and header.



回答3:

Looks like it is the library you are using that is forcing the redirect. I am not sure why but I would probably advise not hacking that. One option would be to store the error in flash data from the class that is throwing it and then on the page it is redirecting to, if flash data exists then display the error. This way you are not having to worry about accidentally updating simpleSAML and having it break again or other unintended issues.

http://laravel.com/docs/4.2/session#flash-data