Pragmatism in the real world

Improved error handling in Slim 3 RC1

From RC1 of Slim 3, we have improved our error handling. We’ve always had error handling for HTML so that when an exception occurs, you get a nice error page that looks like this:

Slim3 error page

However, if you’re writing an API that sends and expects JSON, then it still sends back HTML:

Slim3 old json error

At least we set the right Content-Type and status code!

However, this isn’t really good enough. We should send back JSON if the client has asked for JSON. Until RC1, the only way to do this was to register your own error handler:

$c = $app->getContainer();
$c['errorHandler'] = function ($c) {
  return function ($request, $response, $exception) use ($c) {
    $data = [
      'code' => $exception->getCode(),
      'message' => $exception->getMessage(),
      'file' => $exception->getFile(),
      'line' => $exception->getLine(),
      'trace' => explode("\n", $exception->getTraceAsString()),
    ];

    return $c->get('response')->withStatus(500)
             ->withHeader('Content-Type', 'application/json')
             ->write(json_encode($data));
  };
};

which provides the correct output:

Slim 3 custom json error

However, we can do better than this and do it for you. Starting with RC1, Slim will now output JSON (or XML) when an error occurs if the client’s Accept header is application/json (or application/xml) and it will also provide all the previous exception too. This is much nicer and also works for the two other error handlers: notFound and notAllowed.

Slim 3 rc1 error json

Finally, Note that you should never use our default errorHandler in production as it leaks too much data! We’ll try and fix this before 3.0 final, though.

5 thoughts on “Improved error handling in Slim 3 RC1

  1. Good Idea!

    I see you provide the full trace as well. Do you do this without restriction? Isn't it too revealing of the inner workings of your application and its location on a server?

  2. I was just wondering if it might be a better move to pre-anything slim have a try catch block, that logs this data, so that for unexpected error messages, a good catch all; returns a sorry page to the user, with clear moving forward instructions.

    You can then send the ops / developers as much data as possible to reproduce the bug via HTTP request, database update, whatever.

    404's etc are much easier to deal with, but a server error, I don't think should ever be overly detailed, as it can run the risk of stack security, switching on users minds to try to troubleshoot an app or service, they frankly know nothing about; or just looking frightening.

    I used to want as much on the page / response as possible, but after a few experiments I quickly adopted the new approach of letting my apps bother me with the details (as they are 100% verifiable, and as useful as you code them to be), and focus on fixing the issue, but more importantly directing users of apps to an appropriate (hopefully pre-designated) location.

  3. Hi,

    What about catching PHP Errors and Fatal Error then transforming them into Exceptions which Slim Can handle ( Or basicly this errors beeing handled by ErrorHandler from Slim) ?

    I was succesfull with PHP Syntax Error but fatal Errors not :( had todo double code.

    Any ideas tips?

    class ErrorHandler
    {
    
        public static function register( $container )
        {
            set_error_handler( [
                __CLASS__,
                'handleErrors'
            ] );
            register_shutdown_function( [
                __CLASS__,
                'handleFatalErrors'
            ] );
        }
    
        // Errors as Exception so we can handle them same for Output, Logging ...
        public static function handleErrors( $code, $message, $file = '', $line = 0 )
        {
            throw new RuntimeException( $message, $code ); // Slim Framework ErrorHandler handles this
        }
    
        public static function handleFatalErrors() // Workaround so it does the same as Slim Framework Error Handler
        {
            $e = error_get_last();
            if ( $e ) { // $e['message'], $e['type']
                ob_clean();
                $headers     = self::parseRequestHeaders();
                $contentType = self::determineContentType( $headers['Accept'] );
                header( "Content-type: " . $contentType . "; charset=UTF-8" );
                header( "HTTP/1.1 500 Internal Server Error" );
    
                switch ( $contentType ) {
                    case 'application/json':
                        $output = self::renderJsonErrorMessage( $e );
                        break;
                    case 'application/xml':
                        $output = self::renderXmlErrorMessage( $e );
                        break;
                    case 'text/csv':
                        $output = self::renderCsvErrorMessage( $e );
                        break;
                    default:
                        $output = self::renderHtmlErrorMessage( $e );
                        break;
                }
                exit( $output );
            }
        }
    
        private static function renderHtmlErrorMessage( $e )
        {
            return "Application error. System administrators have been notified.";
        }
    
        /**
         * Render JSON error
         *
         * @param  Exception $exception
         *
         * @return string
         */
        private static function renderJsonErrorMessage( $e )
        {
    
            return '{"message":"Application error. System administrators have been notified."}';
    
        }
    
        /**
         * Render XML error
         *
         * @param  Exception $exception
         *
         * @return string
         */
        private static function renderXmlErrorMessage( $e )
        {
            return "Application error. System administrators have been notified.";
        }
    
        /**
         * Render CSV error
         *
         * @param  Error
         *
         * @return string
         */
        private static function renderCsvErrorMessage( $e )
        {
            return "Application error. System administrators have been notified.";
        }
    
        private static function parseRequestHeaders()
        {
            $headers = array();
            foreach ( $_SERVER as $key => $value ) {
                if ( substr( $key, 0, 5 )  'HTTP_' ) {
                    continue;
                }
                $header             = str_replace( ' ', '-', ucwords( str_replace( '_', ' ', strtolower( substr( $key, 5 ) ) ) ) );
                $headers[ $header ] = $value;
            }
    
            return $headers;
        }
    
        private static function determineContentType( $acceptHeader )
        {
            $list = explode( ',', $acceptHeader );
    
            foreach ( $list as $type ) {
                if ( in_array( $type, [
                    'application/json',
                    'application/xml',
                    'text/csv',
                    'text/html'
                ] ) ) {
                    return $type;
                }
            }
    
            return 'text/html';
        }
    }
    

Comments are closed.