Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This course is archived!
This tutorial uses a deprecated micro-framework called Silex. The fundamentals of REST are still ? valid, but the code we use can't be used in a real application.

Exposing more Error Details

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

Exposing more Error Details

In our app, we trigger 404 pages by calling the throw404 shortcut function in a controller. Behind the scenes, this throws a NotFoundHttpException, which extends that very special HttpException class. The result is a response with a 404 status code. That magic is part of Silex. But without this, we could have built in our own logic in the exception listener to map certain exception classes to specific status codes.

All exceptions have an optional message, which we can populate by passing an argument to throw404. We’re already including some details that might help the API client.

If you go to /api/programmers/bumblebee in the browser, we get a 404 JSON response, but we don’t see that message. Look back at our exception-handling function. The NotFoundHttpException is not an instance of ApiProblemException, so we fall into the situation where we create the ApiProblem by hand. We’re not using the exception’s message anywhere, so it makes sense we don’t see it:

// src/KnpU/CodeBattle/Application.php

// ...
} else {
    $apiProblem = new ApiProblem($statusCode);
}

Before we fix this, let’s update the 404 scenario. We can use the standard details field to store the exception message:

Scenario: Proper 404 exception on no programmer
  # ...
  And the "detail" property should equal "The programmer fake does not exist!"

To get this working, we’re going to set the details property to be the exception object’s message. But wait! We need to be very very careful. We don’t always want to expose the message. What if some deep exception is thrown from the database? We might be exposing our database structure. That would be awkward.

Instead, let’s only expose the message if the exception is an instance of HttpException:

// src/KnpU/CodeBattle/Application.php
// ...

if ($e instanceof ApiProblemException) {
    $apiProblem = $e->getApiProblem();
} else {
    $apiProblem = new ApiProblem($statusCode);

    if ($e instanceof HttpException) {
        $apiProblem->set('detail', $e->getMessage());
    }
}

This will now include only exceptions that are for things like 404 and 403 responses, which we are usually in charge of creating and throwing. If a deep database exception occurs, it won’t be an HttpException message, so nothing will get exposed.

Run the test to try it out. Awesome! That’s a much more helpful error response.

In your application, you’ll still want to be careful with this. We want to be helpful to the client, but we absolutely don’t want to expose any of our internals. Make sure whatever logic you use here is very solid. #security

Even our logic is a bit loose. For example, if we go to a URL that just doesn’t exist, the client sees “No route found” in the details, which is a bit more than I want to show the user. To fix this, you could show messages from an even smaller set of exception classes. The FOSRestBundle for Symfony has a feature like this.

Big Errors When Developing

Let’s throw a big exception from inside the showAction controller method and see how the tests look:

// src/KnpU/CodeBattle/Controller/Api/ProgrammerController.php
// ...

public function showAction($nickname)
{
    throw new \Exception('I made a mistake!');
    // ...
}

Let’s run just one of the scenarios that uses showAction. To do this, we can point Behat directly at the .feature file and include the line number where the word Scenario: appears:

$ php vendor/bin/behat features/api/programmer.feature:64

When the test fails, it prints out what the client will see, which is our API Problem media type response... but with absolutely no details beyond the 500 status code.

While developing, that’s not helpful. Instead, for 500 errors, I want to continue seeing the big beautiful, normal error page, because it includes the exception message and stacktrace.

Go back to the Application.php file where our exception handler function lives. Most applications have some variable that says whether you’re in debug mode or not. If we are, and the status code is 500, let’s not handle the exception here. Instead, the normal big error page will show:

// src/KnpU/CodeBattle/Application.php
// ...

$this->error(function(\Exception $e, $statusCode) use ($app) {
    // only act on /api URLs
    if (strpos($app['request']->getPathInfo(), '/api') !== 0) {
        return;
    }

    // allow 500 errors to be visible to us in debug mode
    if ($app['debug'] && $statusCode == 500) {
        return;
    }
    // ...
}

For Silex, there’s a debug key on this $app variable, which I set in a bootstrap.php file. You should have something similar in your app’s bootstrap or configuration. Use that! Not seeing your exception information is no fun.

Ok, be sure to remove our Exception message from showAction so our app works again.

Leave a comment!

2
Login or Register to join the conversation
Default user avatar
Default user avatar ayyoub | posted 3 years ago | edited

added this in my listener instead of testing for debug , it works just fine
`
if(!$e instanceof HttpException)

        return;

`

Reply

Hey @ayyoub!

That actually makes a lot of sense! Pretty much the only way for you to end up with a non-500 status code is for there to be an exception that implements HttpExceptionInterface (pretty much all implementations of this, in practice, extend the HttpException class). So +1 from me!

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial uses a deprecated micro-framework called Silex. The fundamentals of REST are still ? valid, but the code we use can't be used in a real application.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "silex/silex": "~1.0", // v1.3.2
        "symfony/twig-bridge": "~2.1", // v2.7.3
        "symfony/security": "~2.4", // v2.7.3
        "doctrine/dbal": "^2.5.4", // v2.5.4
        "monolog/monolog": "~1.7.0", // 1.7.0
        "symfony/validator": "~2.4", // v2.7.3
        "symfony/expression-language": "~2.4" // v2.7.3
    },
    "require-dev": {
        "behat/mink": "~1.5", // v1.5.0
        "behat/mink-goutte-driver": "~1.0.9", // v1.0.9
        "behat/mink-selenium2-driver": "~1.1.1", // v1.1.1
        "behat/behat": "~2.5", // v2.5.5
        "behat/mink-extension": "~1.2.0", // v1.2.0
        "phpunit/phpunit": "~5.7.0", // 5.7.27
        "guzzle/guzzle": "~3.7" // v3.9.3
    }
}
userVoice