How Laravel handle Exception
Bài đăng này đã không được cập nhật trong 5 năm
Giới thiệu
Trong một app Laravel mặc định, có hai Kernel, một dùng để xử lý HTTP requests app\Http\Kernel.php
và Kernel còn lại dùng để xử lý console command app\Console\Kernel.php
, mỗi Kernel có một method handle()
nhận input từ HTTP request hoặc input từ cmd (terminal), xử lý và trả về kết quả.
Toàn bộ quá trình xử lý handling được bao bọc bởi bên trong cặp try...catch
:
https://github.com/laravel/framework/blob/5.7/src/Illuminate/Foundation/Http/Kernel.php#L111
try {
$response = $this->processTheInput($input);
} catch (Exception $e) {
// Call report() method of App\Exceptions\Handler
$this->reportException($e);
// Call render() method of App\Exceptions\Handler
$response = $this->renderException($request, $e);
}
return $response;
Như bạn thấy, tất cả exception mà chưa được catch trong app sẽ được catch tại đây và được Laravel xử lý qua 2 bước:
- Report: báo cáo exception tới các channels khác nhau
- Render: convert exception về dạng có thể hiển thị được và hiển thị cho người sử dụng
Có một số trường hợp exception được Laravel xử lý theo một cách khác, ví dụ trong một queue worker chạy ngầm, để ngăn không cho script bị exit khi gặp exception, Laravel catch exception và chỉ report exception chứ không render:
https://github.com/laravel/framework/blob/5.7/src/Illuminate/Queue/Worker.php#L274
try {
// Get new job or execute a job
} catch (Exception $e) {
$this->exceptions->report($e);
}
Trong trường hợp các error, exception khác của PHP (error, warning, fatal error exceptions...), Laraver cũng thiết lập cho PHP report tất cả các loại error, và xử lý bằng custom handler:
// Turn on error reporting
error_reporting(-1);
// Register a custom error handler
set_error_handler([$this, 'handleError']);
// Register a custom exception handler
set_exception_handler([$this, 'handleException']);
// Handle when the script is done
register_shutdown_function([$this, 'handleShutdown']);
Report Exceptions
Khi có một exception xảy ra, việc đầu tiên nên làm đó là log lại stack trace của exception đó, để có thể truy vết và tìm ra nguyên nhân.
Theo mặc định, tất cả exceptions được log lại trong thư mục storage/logs
và chia file log theo từng ngày. Chúng ta có thể kiểm tra những file này bất cứ lúc nào để biết có lỗi nào xảy ra không.
Nhưng trong một số trường hợp, chúng ta không cần quan tâm nếu có exception được ném ra. Vì thế trước khi report exception, Laravel check xem bạn có thực sự muốn report exception đó hay không. Thực tế thì Laravel cũng mặc định loại bỏ report một số exception như authentication, validation exception.
Bạn có thể xem các exception không cần được report trong file:
https://github.com/laravel/framework/blob/5.7/src/Illuminate/Foundation/Exceptions/Handler.php#L57
protected $internalDontReport = [
AuthenticationException::class,
AuthorizationException::class,
HttpException::class,
HttpResponseException::class,
ModelNotFoundException::class,
TokenMismatchException::class,
ValidationException::class,
];
Bạn cũng có thể tự định nghĩa mà bạn không muốn report, thông qua property $dontReport
trong class App\Exceptions\Handler.php
:
https://github.com/laravel/laravel/blob/master/app/Exceptions/Handler.php#L15
class Handler extends ExceptionHandler
{
protected $dontReport = [
MyCustomException::class
];
}
Laravel include một số report channel được xây dựng dựa trên thư viện Monolog:
- Single file log: Tất cả exception được log vào file
storage/logs/laravel.log
- Daily log files: File log được chia theo từng ngày
- Syslog
- errorlog
- Slack
- Papertrail
- stderr
Ngoài ra còn có một channel đặc biệt đó là stack
cho phép bạn sử dụng nhiều channel cùng 1 lúc, chẳng hạn bạn có thể vừa log vào file vừa report exception đến Slack: https://github.com/laravel/laravel/blob/master/config/logging.php
Chúng ta cũng có thể report exception theo cách riêng theo 2 cách:
- Thay đổi logic bên trong method
report()
của classApp\Exceptions\Handler
, method này được gọi ngay khi có một exception cần được report. Nếu muốn custom lại toàn bộ việc report exception, chúng ta có thể bỏ qua lời gọi đếnparent::report($exception)
. - Hoặc bạn có thể định nghĩa 1 method
report()
bên trong custom exception class. Lưu ý là khi dùng cách này, custom exception này sẽ không được log vào file log (https://github.com/laravel/framework/blob/5.7/src/Illuminate/Foundation/Exceptions/Handler.php#L96)
Render Exceptions
Tương tự như report, khi render exception, Laravel cũng check xem exception có method render()
không, vì thế chúng ta có thể custom việc render của 1 exception tương tự như report, trong method render()
này chúng ta có thể return response giống như một controller method bình thường (https://github.com/laravel/framework/blob/5.7/src/Illuminate/Foundation/Exceptions/Handler.php#L168).
Laravel convert exception thành định dạng có thể hiển thị được như HTML hoặc JSON, bước đầu tiên là convert sang HTTPException
:
if ($e instanceof ModelNotFoundException) {
$e = new NotFoundHttpException($e->getMessage(), $e);
} elseif ($e instanceof AuthorizationException) {
$e = new HttpException(403, $e->getMessage());
} elseif ($e instanceof TokenMismatchException) {
$e = new HttpException(419, $e->getMessage());
}
Tiếp theo là handle một số exception đặc biệt, chẳng hạn Illuminate\Http\Exceptions\HttpResponseException
, exception này đã chứa response rồi nên Laravel return response đó luôn.
Authentication Exception Illuminate\Auth\AuthenticationException
được handle bằng method unauthenticated()
trong handler, mặc định nó redirect user về trang /login
trong trường hợp response mong muốn là HTML hoặc trả về JSON object với status 401:
{"message" : "Unauthenticated."}
Trong trường hợp Validation Exception Illuminate\Validation\ValidationException
, Laravel redirect user quay về trang trước đó kèm theo các giá trị input cũ và một error bag $errors
giúp chúng ta có thể hiển thị thông tin lỗi validation:
@if (count($errors) > 0)
<div class="alert alert-danger">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
Trong trường hợp response mong muốn là JSON (request header Accept: application/json
) thì Laravel sẽ respone với status là 422 kèm theo một JSON object:
{
"message": "The given data failed to pass validation.",
"errors": {
"name": [
"The name field is required.",
"The name field must be a string."
]
}
}
Đối với các exception khác, Laravel check response mà client mong muốn là dạng HTML hay JSON để trả về respone tương ứng.
Laravel sử dụng method expectsJson()
của class Illuminate\Http\Request
để kiểm tra client có đang request JSON hay không: https://github.com/laravel/framework/blob/5.7/src/Illuminate/Http/Concerns/InteractsWithContentTypes.php#L42
Trong trường hợp app ở chế độ debug config('app.debug') === true
Laravel convert exception thành JSON theo dạng:
{
"message": "...",
"file": "...",
"line": 0,
"trace": "..."
}
Điều này giúp chúng ta debug trong môi trường development. Tất nhiên là app.debug
không nên được set là true trên môi trường production vì nó có thể để lộ những thông tin nhạy cảm, trong trường hợp này Laravel sẽ chỉ trả về 1 message chung chung:
{
"message": "..."
}
Tuy nhiên với trường hợp exception không phải là HTTPException
, Laravel sẽ chỉ respone với message "Server Error" kèm theo status là 500:
{
"message": "Server Error"
}
Nếu bạn muốn hiển thị message cho client đang sử dụng API thì hãy ném ra HTTPException
, còn không thì Laravel sẽ bảo vệ data của bạn bằng cách ẩn message thực sự của exception và chỉ hiển thị Server Error
.
Với trường hợp HTML response, đầu tiên Laravel check file view trong thư mục resources/views/errors
có tên tương ứng với HTTP status code, chẳng hạn 500.blade.php
, nếu có Laravel sẽ render file view đó và hiển thị cho user.
Nếu không có file view, Laravel sử dụng Symfony exception handler hoặc Whoops error handler để hiển thị stack trace của exception nếu app.debug
đang được bật, hoặc nếu không sẽ chỉ có một message quen quen Whoops, something went wrong on our servers
được hiển thị cho user.
References
All rights reserved