slimphp / Slim

Slim is a PHP micro framework that helps you quickly write simple yet powerful web applications and APIs.
http://slimframework.com
MIT License
11.94k stars 1.95k forks source link

Slim 4 - How to handle errors by ourselves completely? #2757

Closed lautiamkok closed 5 years ago

lautiamkok commented 5 years ago

How can we handle errors by ourselves completely?

I get this error below:

Uncaught Slim\Exception\HttpNotFoundException: Not found.

If I remove these lines:

// $callableResolver = $app->getCallableResolver();
// $responseFactory = $app->getResponseFactory();
// $errorMiddleware = new ErrorMiddleware($callableResolver, $responseFactory, true, true, true);
// $app->add($errorMiddleware);

Then, when we try to catch to the error through a middleware:

$app->add(function (Request $request, RequestHandler $handler) {

    try {
        $response = $handler->handle($request);
        $existingContent = (string) $response->getBody();
        ...
    } catch (\Exception $error) {
        $response = $handler->handle($error->getRequest());
        $data = [
            "status" => $error->getCode(),
            "messsage" => $error->getMessage()
        ];
        $payload = json_encode($data);
        $response->getBody()->write($payload);

        return $response
          ->withHeader('Content-Type', 'application/json')
          ->withStatus($status);
    };

});

We get the same error back:

Uncaught Slim\Exception\HttpNotFoundException: Not found.

We are trying to make our custom error in the JSON format, for example:

{"status":404,"message":"page not found"}

Any ideas?

adriansuter commented 5 years ago

By calling the handle() method of the handler in the catch-block again, the handler would throw the exception again. But this time, no one is there to catch this second exception.

In Slim 4 you could either define your custom error handler (and pass it to the ErrorMiddleware) or simply define custom error renderers (based on accepted content-types by your request or on a forced content-type).

More information can be found in the docs, currently in development: https://github.com/slimphp/Slim-Website/blob/gh-pages-4.x/docs/v4/middleware/error-handling.md

lautiamkok commented 5 years ago

@adriansuter none of that actually works. giving up...

In my view, this should be made simpler. But it is way too complicated now.

l0gicgate commented 5 years ago

@lautiamkok it does work, I think you're doing something wrong on your end. Where are you adding this middleware? It should be the very last middleware you add, right before that middleware should be the routing middleware, otherwise Slim automatically appends the RoutingMiddleware if it hasn't been appended manually, which means it would go in front of your error handling middleware, which means you wouldn't catch anything.

lautiamkok commented 5 years ago

@l0gicgate let me check later again and get back to you later. there are some errors in the code in the doc by the way

slim should have left all the error handling alone from the year dot. how it handles errors just does not make sense. for example in slim 3.12, if i want to handle 404 and other errors myself, I have to write two blocks of code:

$container = new \Slim\Container();
$container['errorHandler'] = function ($container) {
    return function ($request, $response, $exception) use ($container) {
        // print_r(get_class_methods($exception));
        $message = $exception->getMessage();
        $code = $exception->getCode();

        $data = [
            "status" => $code,
            "message" => $message
        ];
        $payload = json_encode($data);
        $response->getBody()->write($payload);

        return $response
            ->withHeader('Content-Type', 'application/json')
            ->withStatus($code);
    };
};
$container['notFoundHandler'] = function ($container) {
    return function ($request, $response) use ($container) {
        return $response->withStatus(404)
            ->withHeader('Content-Type', 'text/html')
            ->write('Page not found');
    };
};

It should just be one block of code. also, errorHandler and notFoundHandler work differently. they should work the same way.

lautiamkok commented 5 years ago

My personal view on these lines below for handling error in slim 4:

$callableResolver = $app->getCallableResolver();
$responseFactory = $app->getResponseFactory();
$errorMiddleware = new ErrorMiddleware($callableResolver, $responseFactory, true, true, true);
$app->add($errorMiddleware);

I am not sure if this is a PSR standard or not but it is too ugly to read and to understand what is going on.

I don't know what these two $callableResolver and $responseFactory are doing there. if they are not helping us to understand by reading them, they shouldn't be there.

this is just my opinion.

adriansuter commented 5 years ago

That is dependency injection. As the middleware needs to have the callable resolver and the response factory, they would be given to the middleware. That way, the middleware is as independent as possible from the rest.

l0gicgate commented 5 years ago

I’m closing this as resolved. The docs are clear. If you dislike the architecture you can always use a different framework. You’re also more than welcome to contribute when those architecture decisions are being made, the PRs and discussions are public.

lightswitch05 commented 4 years ago

Another way to handle this is to define a default route after all your other route definitions:

    $app->any('{route:.*}', function(Request $request, Response $response) {
        $response = $response->withStatus(404, 'page not found');
        return $response;
    });
b-hayes commented 2 years ago

I think a good answer would be to show exactly how a way to disable ALL error handling of any kind since I'm sure slim is already using specific exception types internally that can easily be caught. Or show how to specify a single error handler for all events so a controller view can be used and just display a 404/500 or whatever else message it needs to show accordingly.

Just saying docs are clear with no reference and if you don't like it move along isn't a great response.

If there is no way to do this I'd agree it is just too complicated. Error handling is so simple.

skizzo commented 3 weeks ago

I'm unsuccessfully searching for a way to achieve what was so easily possible in Slim 3 – catching any kind of exception caused when calling a route, handling it myself and then displaying a JSON response with a custom HTTP status code, like mentioned in https://github.com/slimphp/Slim/issues/2757#issuecomment-512360043.

Could anybody please point me into the right direction, it's crazy that this standard feature is not even mentioned in the docs. Thanks.

odan commented 3 weeks ago

@skizzo It is possible. Just don't add the ErrorHandlingMiddleware and replace it with your own middleware that catches all Throwables and/or other more specific Exceptions.

skizzo commented 3 weeks ago

@odan , sorry but your "now draw the rest of the f***king owl" answer is not helpful at all. The issue subject literally says "how" and not "if" this is possible, and nobody here even bothered.

So in any case anybody else stumbles over this issue after hours of searching the web (for something that should be properly documented in the first place), I finally got it working like this in Slim 4.14 (PHP 8.x):


Step 1: Install a PSR-7 Implementation

We need to install a PSR-7 implementation, e.g.nyholm/psr7 with composer require nyholm/psr7.


Step 2: Create custom Error Handler

use Nyholm\Psr7\Factory\Psr17Factory;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Slim\Interfaces\ErrorHandlerInterface;

class NBSlimErrorHandler implements ErrorHandlerInterface
{
  // invoked from outside of Slim
  public static function logException(Throwable $exception)
  {
    $statusCode = 500; // or anything else
    $errorData  = NBSlimErrorHandler::getExceptionData($exception);

    // feel free to use your own logger here
    NBLogger::error($errorData["message"], [
      "message" => $errorData["message"],
      "file"    => $errorData["file"],
      "line"    => $errorData["line"],
      "trace"   => $errorData["trace"],
    ]);

    header('Content-Type: application/json');
    http_response_code(500);
    echo json_encode([
      "success" => false,
      "error"   => $errorData,
    ]);
    exit();
  }

  // invoked by Slim
  public function __invoke(ServerRequestInterface $request, Throwable $exception, bool $displayErrorDetails, bool $logErrors, bool $logErrorDetails): ResponseInterface
  {
    $statusCode = 500; // or anything else
    $errorData  = NBSlimErrorHandler::getExceptionData($exception);

    // feel free to use your own logger here
    NBLogger::error($errorData["message"], [
      "message" => $errorData["message"],
      "file"    => $errorData["file"],
      "line"    => $errorData["line"],
      "trace"   => $errorData["trace"],
    ]);

    $psr17Factory = new Psr17Factory();
    $response     = $psr17Factory->createResponse($statusCode);
    $response->getBody()->write(json_encode([
      "success" => false,
      "error"   => $errorData,
    ]));
    return $response->withStatus($statusCode)->withHeader('Content-Type', 'application/json');
  }

  // Helper Function to extract data from an exception
  public static function getExceptionData(Throwable $exception)
  {
    $stripPath = getPublicHtmlFolder(); // see below

    $message = $exception->getMessage();
    $file    = implode("", explode($stripPath, $exception->getFile())); // remove $stripPath
    $line    = $exception->getLine();
    $trace   = $exception->getTraceAsString();
    $trace   = explode("\n", substr(implode("", explode($stripPath, $trace)), 1));
    return [
      "message" => $message,
      "file"    => $file,
      "line"    => $line,
      "trace"   => $trace,
    ];
  }

}

// for errors outside of a Slim Route handler
function log_exception(Throwable $exception)
{
  NBSlimErrorHandler::logException($exception);
}
set_exception_handler("log_exception");
ini_set("display_errors", "off");
error_reporting(E_ALL);

// helper function to determine the public_html folder
function getPublicHtmlFolder()
{
  $dirname        = dirname(__DIR__);
  $search         = "/public_html/";
  $pathPublicHtml = mb_substr($dirname, 0, mb_strpos($dirname, $search) + mb_strlen($search));
  return $pathPublicHtml;
}

Step 3: Use custom Error Handler in Slim App

// ...
$app = AppFactory::create();
$app->setBasePath("/your/base/path");

// Use custom Error Handler
$errorMiddleware = $app->addErrorMiddleware(true, true, true);
$errorMiddleware->setDefaultErrorHandler(NBSlimErrorHandler::class);

// Add Routing Middleware
$app->addRoutingMiddleware();
// ...
odan commented 3 weeks ago

@skizzo

  1. I explained one way of how this could work, if you would be able to read my answer.
  2. This is not a free development / coding service here.
  3. You should learn how to communicate in a respectful way.
skizzo commented 3 weeks ago

I apologize for my tone, but it's just frustrating that issues like these are being closed with reasoning that sounds like "RTFM" when the docs obviously don't explain such a basic use case scenario – which is why this issue was created in the first place.

b-hayes commented 3 weeks ago

I get your frustration @skizzo. If its so clear then why is it so hard for anyone to say it. Thanks for posting your solution im sure it will help someone. Feel bad for not posting what I did to resolve this now.