Original link: learnku.com/laravel/t/3… For discussion, head to the professional Laravel developer forum: learnku.com/Laravel

Api-based project development is becoming more and more popular, and can be easily implemented using Laravel. However, the topic of how to handle various exceptions is rarely mentioned. As a result, API users often complain that they receive few more error messages than Server errors. So, how do we gracefully handle API errors to make them more readable?


Target: status code + error message

Correct error descriptions are even more important for API development than Web browser-based projects. As users, we can also use browser messages to clearly understand the error and what to do about it. But for the apis themselves, they are used by software, not humans, so the returned result should be readable by Machines. This means that an HTTP status code is essential.

The API returns a status code for each request, usually 200 for success, or some other status code starting with 2.

If an error response is returned, the response should not contain 2XX code. The following are the most common error codes:

| status code description | | | | 404 not found resources (requested does not exist) | | 401 | not certification (need to login) | | 403 | no permissions | | 400 | wrong request (URL or parameter is not correct) | | 422 | | validation failure | 500 | | server error

Note: Laravel will automatically specify a status code if no status code is added when the response is returned, but there is no guarantee that the specified status code is correct. It’s best to add the correct status code yourself.

In addition, we need to consider Human-readable Messages. Therefore, a typical response would contain HTTP error code and JSON results, as follows:

{
    "error": "Resource not found"
}
Copy the code

Ideally, it should contain more details to help API consumers deal with errors. Here’s an example of how the Facebook API returns an error:

{
  "error": {
    "message": "Error validating access token: Session has expired on Wednesday, 14-Feb-18 18:00:00 PST. The current time is Thursday, 15-Feb-18 13:46:35 PST."."type": "OAuthException"."code": 190,
    "error_subcode": 463,
    "fbtrace_id": "H2il2t5bn4e"}}Copy the code

Often, the wrong content is what needs to be displayed in a browser or mobile device. It is therefore best to provide as much detail as necessary.

Now, let’s see how we can better improve our API error prompts.

Tip 1. Toggle APP_DEBUG=false even locally

Laravel’s.env file has an important setting, APP_DEBUG, which can be false or true.

If set to true, all errors are displayed along with details, including class names, database tables, and so on.

This is a huge security issue, so setting it to false is highly recommended in a production environment.

However, I recommend turning it off even locally for API projects for the following reasons.

When you turn off actual errors, you are forced to think like API users, because they will only receive Server errors (returning Server errors) and no more information. In other words, this is when you need to think about how to handle errors and provide appropriate response messages.


Tip 2: Unprocessed route-rollback method

Case one – what if someone calls an API that doesn’t exist, and someone even enters the wrong address in the URL. By default, you get the following response from the API:

Request URL: http://q1.test/api/v1/offices
Request Method: GET
Status Code: 404 Not Found
{
    "message": ""
}
Copy the code

At least 404 response succeeded. You can do a better job of explaining the error with a message.

You can do this by specifying the Route::fallback() method at the end of routes/api.php to handle all requests to access routes that do not exist.

Route::fallback(function() {return response()->json([
        'message'= >'Page Not Found. If error persists, contact [email protected]'], 404);
});
Copy the code

The result is the same 404 response, but now there is an error message that provides more information about how to handle the error.

Tip 3. Override 404 ModelNotFoundException

The most common is the failure to find some Model object, usually thrown by Model :: findOrFail($ID). Here’s a typical message your API will display:

{
    "message": "No query results for model [App\\Office] 2"."exception": "Symfony\\Component\\HttpKernel\\Exception\\NotFoundHttpException". }Copy the code

This is true, but the message displayed to the end user is not very pretty, so my recommendation is to rewrite the handling of that particular exception.

We can in the app/Exceptions/Handler. PHP (please remember this file, we will return to it in the future many times) used in the render () method:

// Don't forget this in the beginning of file use Illuminate\Database\Eloquent\ModelNotFoundException; / /... public function render($request, Exception $exception) { if ($exception instanceof ModelNotFoundException) { return response()->json([ 'error'= >'Entry for '.str_replace('App\\', '', $exception->getModel()).' not found'], 404); } return parent::render($request, $exception); }Copy the code

We can catch any number of exceptions in this method. In this case, we’ll return the same 404 code, but with more readability:

{
    "error": "Entry for Office not found"
}
Copy the code

Note: Have you noticed an interesting method? $exception – > getModel ()? We can get a lot of useful information from the $Exception object. Here is a screenshot of the PhpStorm autocomplete:

Tip 4: Capture as much information as possible in validation

Developers tend not to worry too much about validation rules, and instead stick to simple rules such as Required, Date, and EMai. But for apis, the most typical cause of errors is actually the submission of invalid data by consumers.

If we don’t make more effort to collect the unvalidated data, the API will pass the backend validation and throw a simple Server error without any details (actually the cause is a database query error).

Let’s take a look at this example – we have a store() method in Controller:

public function store(StoreOfficesRequest $request)
{
    $office = Office::create($request->all());

    return (new OfficeResource($office))
        ->response()
        ->setStatusCode(201);
}

Copy the code

Our FormRequest file/app/Http Requests/StoreOfficesRequest. PHP include two rules:

public function rules()
{
    return [
        'city_id'= >'required|integer|exists:cities,id'.'address'= >'required'
    ];
}

Copy the code

If we omit these two parameters and pass null values in them, the API will return a fairly readable error with a **422 ** status code (this status code is generated by default due to Laravel validation failure) :

{
    "message": "The given data was invalid."."errors": { 
        "city_id": ["The city id must be an integer."."The city id field is required."]."address": ["The address field is required."]}}Copy the code

It lists all field errors and mentions all errors in each field, not just the first error caught.

Now, if we do not specify those validation rules and allow validation to pass, here is what the API returns:

{
    "message": "Server Error"
}
Copy the code

Just a server error, no other useful information about what is wrong and what fields are missing or incorrect. So the API users are confused.

So I’ll repeat my point here – try to capture as many possible scenarios as possible in your validation rules. Check for field presence, type, minimum-maximum, duplicate, and so on

Tip 5 An empty 500 server error can usually be avoided using a try-catch

Continuing with the example above, the worst thing you can do with an API is null errors. But everything can go wrong, especially in large projects, and we can’t fix or predict random errors.

But we can catch them! Use a try-catch PHP block.

Imagine this controller code:

public function store(StoreOfficesRequest $request)
{
    $admin = User::find($request->email);
    $office = Office::create($request->all() + ['admin_id'= >$admin->id]);
    (new UserService())->assignAdminToOffice($office);

    return (new OfficeResource($office))
        ->response()
        ->setStatusCode(201);
}
Copy the code

This is a fictional example, and it’s very common. Search for users by E-mail, then create a record and act on that record. And at any step, mistakes can happen. The E-mail message may be empty, the administrator may not be found (or the administrator who found the error), the service method may throw any other errors or exceptions, and so on.

There are many ways to handle and use a try-catch, but one of the most popular is to simply catch a large try-catch and correspond to which exception class threw it:

try {
    $admin = User::find($request->email);
    $office = Office::create($request->all() + ['admin_id'= >$admin->id]);
    (new UserService())->assignAdminToOffice($office);
} catch (ModelNotFoundException $ex) { // User not found
    abort(422, 'Invalid email: administrator not found');
} catch (Exception $ex) { // Anything that went wrong
    abort(500, 'Could not create office or assign it to administrator');
}
Copy the code

This way, we can call abort() at any time and add the desired error message. If we did this in each controller (or most of them), our API would return the same 500 as Server Error, but with more actionable error messages.

Tip 6 Handle third-party API errors by catching exceptions

Today, Web projects use a lot of external apis, and they can fail. If their API is good, they will provide the appropriate exception and error mechanism, so we need to use it in our application.

For example, do a Guzzle curl request on some URL and catch exceptions.

The code is simple:

$client = new \GuzzleHttp\Client();
$response = $client->request('GET'.'https://api.github.com/repos/guzzle/guzzle123456'); / /... What do you do with that responseCopy the code

You may have noticed that the Github URL is invalid and the repository does not exist. Also, if we leave the code as it is, our API will throw a 500 Server error, with no further details. But we can catch exceptions and provide consumers with more details:

// Use GuzzleHttp\Exception\RequestException at the top; / /... try {$client = new \GuzzleHttp\Client();
    $response = $client->request('GET'.'https://api.github.com/repos/guzzle/guzzle123456');
} catch (RequestException $ex) {
    abort(404, 'Github Repository not found');
}

Copy the code

Tip 6.1 Creating Your Own Exception

We can even go one step further and create our own exceptions, especially those related to some third-party API errors.

php artisan make:exception GithubAPIException
Copy the code

Then, we newly generated files app/Exceptions/GithubAPIException. PHP will be as follows:

namespace App\Exceptions;

use Exception;

class GithubAPIException extends Exception
{

    public function render() {/ /... }}Copy the code

We can even make it null, but throw it as an exception. Even an exception name can help API users avoid future errors. So here’s what we do:

try {
    $client = new \GuzzleHttp\Client();
    $response = $client->request('GET'.'https://api.github.com/repos/guzzle/guzzle123456');
} catch (RequestException $ex) {
    throw new GithubAPIException('Github API failed in Offices Controller');
}
Copy the code

Not only that – we can move error handling to the app/Exceptions/handler.php file (remember above?). , as follows:

public function render($request, Exception $exception)
{
    if ($exception instanceof ModelNotFoundException) {
        return response()->json(['error'= >'Entry for '.str_replace('App\\'.' '.$exception->getModel()).' not found'], 404);
    } else if ($exception instanceof GithubAPIException) {
        return response()->json(['error'= >$exception->getMessage()], 500);
    } else if ($exception instanceof RequestException) {
        return response()->json(['error'= >'External API call failed.'], 500);
    }

    return parent::render($request.$exception);
}

Copy the code

One final note

That’s my technique for handling API errors, but it’s not a hard and fast rule. Everyone can have their own opinion, if you have some of your own, feel free to comment and discuss it below.

Finally, in addition to error handling, I want to encourage you to do two things:

  • To provide detailed API documentation for users, use a package API Generator similar to the following.
  • Return API error using third party service Bugsnag/Sentry/Rollbar. They are not free, but can save a lot of time when debugging.

Original link: learnku.com/laravel/t/3… For discussion, head to the professional Laravel developer forum: learnku.com/Laravel