Như title, thường thì khi bắt tay vào làm một project mới, các dev sẽ quan tâm làm thế nào để nó chạy đầu tiên, sau đó là đến clean code rồi Unit Test các thứ, nhưng dường như có một vấn đề đã bị khá là nhiều dev bỏ quên, đó là xử lý các lỗi và Exception phát sinh khi sản phẩm đã lên Production. Việc này sẽ dẫn đến 2 vấn đề:

  1. Người dùng khó chịu và không hiểu ứng dụng đang gặp vấn đề gì, ví dụ như khi click vào 1 button nào đó lại nhận được dòng thông báo to tướng: Undefined Index abc xyz ?? 😃 ?? chắc lúc đấy ngáo cmnl
  2. Dễ để lộ các thông tin về mã nguồn hoặc CSDL, ví dụ như cái error: Cannot Insert into column abc gì đó not null chẳng hạn. Bằng những thứ đơn giản như vậy mà tin tặc có thể sử dụng để tấn công hệ thống

Vậy nên hãy hạn chế những điều này bằng một số cách dưới đây.

1. Đặt APP_DEBUG=false

Mở file .env lên là bạn có thể nhìn thấy cài đặt này ngay. Nếu bạn đặt nó là true thì Laravel sẽ trả về lỗi một cách khá là chi tiết bao gồm cả các class hay DB tables, ... ![](https://images.viblo.asia/31ee796e-9dfc-48a8-9017-de9d62b9a5c2.png

Như đã nói ở trên, việc này gây ra các vấn đề cực kỳ lớn về bảo mật. Rất nhiều người đã quên tắt chế độ này khi đưa project lên production nên để an toàn, hãy tắt luôn nó ngay cả khi bạn đang trong quá trình phát triển, hãy chỉ nên bật khi cần thiết. Lý do của việc này là để giúp bạn suy nghĩ giống như một người dùng bình thường khi chỉ nhận được dòng chữ "Server error" và không có thông tin gì thêm. Nói cách khác, bạn sẽ buộc phải suy nghĩ cách xử lý lỗi và học cách tự mình nghĩ ra các thông báo lỗi phù hợp trong từng trường hợp.

2. Fallback Method

Đây mới là tình huống đầu tiên và cũng là thường gặp nhất, đó là khi ai đó gọi đến một API route không tồn tại, ví dụ người đấy gõ nhầm link chẳng hạn, theo như mặc định thì Laravel sẽ trả về kiểu như này:

Request URL: http://test/api/v1/get-something
Request Method: GET
Status Code: 404 Not Found
{
    "message": ""
}

Cơ bản thì thế này là được rồi nhưng bạn có thể làm cách khác để giải thích rõ ràng hơn kèm một đoạn message bằng cách sử dụng phương thức Route::fallback() đặt cuối cùng tại routes/api.php, phương thức này sẽ xử lý toàn bộ các routes không đúng:

Route::fallback(function(){
    return response()->json([
        'message' => 'Page Not Found. Check if you entered an invalid link!'
    ], 404);
});

Vẫn là code lỗi 404 thôi nhưng giờ message đã có ý nghĩa hơn nhiều rồi, bằng cách này người dùng sẽ có thêm thông tin cần phải làm gì kế tiếp.

3. Override 404 ModelNotFoundException

Một trong những exception hay gặp phải nhất là có model nào đó not found, thường được throw bởi Model::findOrFail($id). Nếu ta cứ để nguyên si như vậy, một exception sẽ được bắn ra ngoài API như sau:

{
    "message": "No query results for model [App\\AbcModel] 2",
    "exception": "Symfony\\Component\\HttpKernel\\Exception\\NotFoundHttpException",
    ...
}

Việc này thường gặp ở các bạn newbie khi các bạn không biết rằng method findOrFail() sẽ trả về một exception nếu không tìm được bản ghi phù hợp thay vì trả về rỗng.

Cái này không sai nhưng nó không phù hợp với những người dùng cuối, những người không có hiểu biết về công nghệ, hơn nữa nó còn khiến hệ thống bị mất an toàn bảo mật. Vậy nên hãy nhớ try {} catch() {} đầy đủ với những phương thức như thế này.

Tuy nhiên vấn đề lại nảy sinh ở đây, mình là một người rất lười nên mình sẽ ghét phải sử dụng try catch ở mọi nơi mọi chỗ như vậy, đế lúc debug cũng sẽ rất là khổ, vậy nên lời khuyên là hãy ghi đè nó một lần duy nhất, để từ những lần sử dụng sau nó sẽ cho ra một message có nghĩa như sau:

Mở file app/Exceptions/Handler.php, render():

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);
}

Từ lần sau khi sử dụng lại findOrFail() mà không có try catch() thì bạn sẽ nhận được Exception với tin nhắn sau đây:

{
    "error": "Entry for AbcModel not found"
}

4. Bắt chặt hơn trong Validation

Một công cụ mà đến ngay cả mình cùng quên không sử dụng rất nhiều, mặc dù nó rất hữu ích, tránh việc phải if else quá nhiều. Bằng cách sử dụng Validation bạn sẽ tránh được việc project bị crash khi người dùng đưa lên một dữ liệu không được phép hoặc đưa thiếu dữ liệu nào đó.

Ngoài ra việc Validate dữ liệu cũng sẽ giúp trả về những message mà người dùng có thể hiểu được để sửa lại kịp thời, ví dụ tôi có một phương thức store() như thế này:

public function store(StorePostsRequest $request)
{
    $post = Post::create($request->all());
    
    return (new PostResource($post))
        ->response()
        ->setStatusCode(201);
}

Thì trong app/Http/Requests/StorePostsRequest.php của chúng ta cũng sẽ có 2 rule tương ứng như sau:

public function rules()
{
    return [
        'post_id' => 'required|integer|exists:cities,id',
        'content' => 'required',
        'author_id' => 'required|integer'
    ];
}

Nếu người dùng nhập thiếu một trong số các dữ liệu trên hoặc nhập sai kiểu dữ liệu thì một message sẽ được gửi ra kèm code lỗi 422 (mặc định của Laravel)

{
    "message": "The given data was invalid.",
    "errors": { 
        "post_id": ["The post id must be an integer.", "The post id field is required."],
        "author": ["The author field is required."]
    }
}

Như vậy người dùng sẽ biết mình đang làm sai cái gì để sửa lại ngay, thay vì việc chỉ nhận được một dòng thông báo:

{
    "message": "Server Error"
}

Và chẳng biết phải làm gì với nó cả!

5. Xử lý 3rd Party API Errors bằng việc bắt Exception của chính nó

Một project Larave không thể không sử dụng các package bên ngoài, và những package này đều là mã nguồn mở nên việc chúng đôi khi xảy ra lỗi là không thể tránh khỏi. Hãy sử dụng try - catch và bắt lấy các exceptions đã được định nghĩa bên trong các package này.

Ví dụ tôi có một đoạn code sử dụng Guzzle để call đến một API lấy dữ liệu:

$client = new \GuzzleHttp\Client();
$response = $client->request('GET', 'https://api.github.com/repos/guzzle/guzzle123456');

Nhưng cay đắng là URL kia không hợp lệ vì repository đó không hề tồn tại. Nếu chúng ta để đoạn code y nguyên thế kia nó sẽ trả ra một thông báo:

{
    "message": "Server Error"
}

Và thật sự là người dùng đến đây cũng sẽ chẳng hiểu lỗi ở chỗ nào nữa, tuy nhiên ta hoàn toàn có thể bắt lấy các exceptions này và khiến cho chúng đầy đủ thông tin hơn:

use GuzzleHttp\Exception\RequestException;
// ...

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');
}

Như vậy người dùng sẽ hiểu để thao tác lại dễ dàng hơn rất nhiều

6. Tự chế Exceptions của riêng mình

Đương nhiên điều này là có thể và nó còn có liên hệ đến một số lỗi của các package thứ 3 nữa. Cách làm như sau:

php artisan make:exception GithubAPIException

Laravel sẽ tự động tạo file exception của bạn theo tại thư mục app/Exceptions, ví dụ của tôi là GithubAPIException trông sẽ như thế này:

namespace App\Exceptions;

use Exception;

class GithubAPIException extends Exception
{
    public function render()
    {
        // ...
    }
}

Bạn có thể để nó trông như vậy cũng được, các exception vẫn sẽ được trả ra bình thường, mục đích của việc này là để trong các try - catch kế tiếp, bạn sẽ biết cụ thể exception đó là của cái gì, nhìn vào ví dụ đây chắc là sẽ hiểu:

// GetGithub class
if (!$foundGithub) {
    throw new GithubAPIException('Github API failed in ABC Service');
}

// other service
try {
    $github = (new GetGithub())->getGithub();
} catch (GithubAPIException $e) {
    // do something...
}

Không những vậy, chúng ta còn có thể đưa nó vào app/Exceptions/Handler.php để đỡ phải dùng try - catch quá nhiều chỗ như vầy:

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);
}

Cuối cùng

Đó là một số mẹo của tôi đễ xử lý các Errors và Exceptions đứng từ phía một người dùng ứng dụng, bạn có thể áp dụng các cách này hoặc xử lý chúng theo cách riêng của bạn, nếu bạn có ý kiến hay hơn vui lòng cho tôi xin mấy comment bên dưới, còn nếu bạn thích bài viết này thì hãy Upvote, Share and Subscribe nhé.

4.0 rồi, làm luôn đi đừng có do dự! 😄