錯誤處理
簡介
When you start a new Laravel project, error and exception handling is already configured for you; however, at any point, you may use the withExceptions
method in your application's bootstrap/app.php
to manage how exceptions are reported and rendered by your application.
The $exceptions
object provided to the withExceptions
closure is an instance of Illuminate\Foundation\Configuration\Exceptions
and is responsible for managing exception handling in your application. We'll dive deeper into this object throughout this documentation.
設定
config/app.php
設定檔中的 debug
選項用來判斷錯誤在實際顯示給使用者時要包含多少資訊。預設情況下,這個選項被設為依照 APP_DEBUG
環境變數值,該環境變數儲存於 .env
檔內。
在本機上開發時,應將 APP_DEBUG
環境變數設為 true
。 在正式環境上,這個值一定要是 false
。若在正式環境上將該值設為 true
,則會有將機敏設定值暴露給應用程式終端使用者的風險。
Handling Exceptions
回報 Exception
In Laravel, exception reporting is used to log exceptions or send them to an external service Sentry or Flare. By default, exceptions will be logged based on your logging configuration. However, you are free to log exceptions however you wish.
If you need to report different types of exceptions in different ways, you may use the report
exception method in your application's bootstrap/app.php
to register a closure that should be executed when an exception of a given type needs to be reported. Laravel will determine what type of exception the closure reports by examining the type-hint of the closure:
1->withExceptions(function (Exceptions $exceptions) {2 $exceptions->report(function (InvalidOrderException $e) {3 // ...4 });5})
1->withExceptions(function (Exceptions $exceptions) {2 $exceptions->report(function (InvalidOrderException $e) {3 // ...4 });5})
When you register a custom exception reporting callback using the report
method, Laravel will still log the exception using the default logging configuration for the application. If you wish to stop the propagation of the exception to the default logging stack, you may use the stop
method when defining your reporting callback or return false
from the callback:
1->withExceptions(function (Exceptions $exceptions) {2 $exceptions->report(function (InvalidOrderException $e) {3 // ...4 })->stop();56 $exceptions->report(function (InvalidOrderException $e) {7 return false;8 });9})
1->withExceptions(function (Exceptions $exceptions) {2 $exceptions->report(function (InvalidOrderException $e) {3 // ...4 })->stop();56 $exceptions->report(function (InvalidOrderException $e) {7 return false;8 });9})
若要為給定的例外自訂 Exception 回報,可使用 Reportable 的例外。
全域 Log 上下文
If available, Laravel automatically adds the current user's ID to every exception's log message as contextual data. You may define your own global contextual data using the context
exception method in your application's bootstrap/app.php
file. This information will be included in every exception's log message written by your application:
1->withExceptions(function (Exceptions $exceptions) {2 $exceptions->context(fn () => [3 'foo' => 'bar',4 ]);5})
1->withExceptions(function (Exceptions $exceptions) {2 $exceptions->context(fn () => [3 'foo' => 'bar',4 ]);5})
Exception Log 的上下文
為所有 Log 訊息都新增額外的上下文可能會很實用,但有些特別的 Exception 可能會有一些獨特的上下文,而我們也想將這類上下文加到 Log 上。只要在 Exception 中定義一個 context
方法,就可以指定與該 Exception 相關的資料,將這些資料包含到例外的 Log 中:
1<?php23namespace App\Exceptions;45use Exception;67class InvalidOrderException extends Exception8{9 // ...1011 /**12 * Get the exception's context information.13 *14 * @return array<string, mixed>15 */16 public function context(): array17 {18 return ['order_id' => $this->orderId];19 }20}
1<?php23namespace App\Exceptions;45use Exception;67class InvalidOrderException extends Exception8{9 // ...1011 /**12 * Get the exception's context information.13 *14 * @return array<string, mixed>15 */16 public function context(): array17 {18 return ['order_id' => $this->orderId];19 }20}
report
輔助函式
Sometimes you may need to report an exception but continue handling the current request. The report
helper function allows you to quickly report an exception without rendering an error page to the user:
1public function isValid(string $value): bool2{3 try {4 // Validate the value...5 } catch (Throwable $e) {6 report($e);78 return false;9 }10}
1public function isValid(string $value): bool2{3 try {4 // Validate the value...5 } catch (Throwable $e) {6 report($e);78 return false;9 }10}
避免重複回報的 Exception
若你在專案中使用 report
函式,偶爾可能會發生同一個 Exception 被回報多次的情況,並在 Log 中造成重複的項目。
If you would like to ensure that a single instance of an exception is only ever reported once, you may invoke the dontReportDuplicates
exception method in your application's bootstrap/app.php
file:
1->withExceptions(function (Exceptions $exceptions) {2 $exceptions->dontReportDuplicates();3})
1->withExceptions(function (Exceptions $exceptions) {2 $exceptions->dontReportDuplicates();3})
現在,當使用相同 Exception 實體來呼叫 report
輔助函式時,就只有第一次呼叫會被回報:
1$original = new RuntimeException('Whoops!');23report($original); // reported45try {6 throw $original;7} catch (Throwable $caught) {8 report($caught); // ignored9}1011report($original); // ignored12report($caught); // ignored
1$original = new RuntimeException('Whoops!');23report($original); // reported45try {6 throw $original;7} catch (Throwable $caught) {8 report($caught); // ignored9}1011report($original); // ignored12report($caught); // ignored
Exception 的 Log 等級
在將訊息寫入專案的 Log 時,這些訊息會以特定的 Log 等級寫入。這個等級即代表該日誌訊息的嚴重程度。
As noted above, even when you register a custom exception reporting callback using the report
method, Laravel will still log the exception using the default logging configuration for the application; however, since the log level can sometimes influence the channels on which a message is logged, you may wish to configure the log level that certain exceptions are logged at.
To accomplish this, you may use the level
exception method in your application's bootstrap/app.php
file. This method receives the exception type as its first argument and the log level as its second argument:
1use PDOException;2use Psr\Log\LogLevel;34->withExceptions(function (Exceptions $exceptions) {5 $exceptions->level(PDOException::class, LogLevel::CRITICAL);6})
1use PDOException;2use Psr\Log\LogLevel;34->withExceptions(function (Exceptions $exceptions) {5 $exceptions->level(PDOException::class, LogLevel::CRITICAL);6})
Ignoring Exceptions by Type
When building your application, there will be some types of exceptions you never want to report. To ignore these exceptions, you may use the dontReport
exception method in your application's bootstrap/app.php
file. Any class provided to this method will never be reported; however, they may still have custom rendering logic:
1use App\Exceptions\InvalidOrderException;23->withExceptions(function (Exceptions $exceptions) {4 $exceptions->dontReport([5 InvalidOrderException::class,6 ]);7})
1use App\Exceptions\InvalidOrderException;23->withExceptions(function (Exceptions $exceptions) {4 $exceptions->dontReport([5 InvalidOrderException::class,6 ]);7})
Alternatively, you may simply "mark" an exception class with the Illuminate\Contracts\Debug\ShouldntReport
interface. When an exception is marked with this interface, it will never be reported by Laravel's exception handler:
1<?php23namespace App\Exceptions;45use Exception;6use Illuminate\Contracts\Debug\ShouldntReport;78class PodcastProcessingException extends Exception implements ShouldntReport9{10 //11}
1<?php23namespace App\Exceptions;45use Exception;6use Illuminate\Contracts\Debug\ShouldntReport;78class PodcastProcessingException extends Exception implements ShouldntReport9{10 //11}
Internally, Laravel already ignores some types of errors for you, such as exceptions resulting from 404 HTTP errors or 419 HTTP responses generated by invalid CSRF tokens. If you would like to instruct Laravel to stop ignoring a given type of exception, you may use the stopIgnoring
exception method in your application's bootstrap/app.php
file:
1use Symfony\Component\HttpKernel\Exception\HttpException;23->withExceptions(function (Exceptions $exceptions) {4 $exceptions->stopIgnoring(HttpException::class);5})
1use Symfony\Component\HttpKernel\Exception\HttpException;23->withExceptions(function (Exceptions $exceptions) {4 $exceptions->stopIgnoring(HttpException::class);5})
轉譯 Exception
By default, the Laravel exception handler will convert exceptions into an HTTP response for you. However, you are free to register a custom rendering closure for exceptions of a given type. You may accomplish this by using the render
exception method in your application's bootstrap/app.php
file.
The closure passed to the render
method should return an instance of Illuminate\Http\Response
, which may be generated via the response
helper. Laravel will determine what type of exception the closure renders by examining the type-hint of the closure:
1use App\Exceptions\InvalidOrderException;2use Illuminate\Http\Request;34->withExceptions(function (Exceptions $exceptions) {5 $exceptions->render(function (InvalidOrderException $e, Request $request) {6 return response()->view('errors.invalid-order', status: 500);7 });8})
1use App\Exceptions\InvalidOrderException;2use Illuminate\Http\Request;34->withExceptions(function (Exceptions $exceptions) {5 $exceptions->render(function (InvalidOrderException $e, Request $request) {6 return response()->view('errors.invalid-order', status: 500);7 });8})
You may also use the render
method to override the rendering behavior for built-in Laravel or Symfony exceptions such as NotFoundHttpException
. If the closure given to the render
method does not return a value, Laravel's default exception rendering will be utilized:
1use Illuminate\Http\Request;2use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;34->withExceptions(function (Exceptions $exceptions) {5 $exceptions->render(function (NotFoundHttpException $e, Request $request) {6 if ($request->is('api/*')) {7 return response()->json([8 'message' => 'Record not found.'9 ], 404);10 }11 });12})
1use Illuminate\Http\Request;2use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;34->withExceptions(function (Exceptions $exceptions) {5 $exceptions->render(function (NotFoundHttpException $e, Request $request) {6 if ($request->is('api/*')) {7 return response()->json([8 'message' => 'Record not found.'9 ], 404);10 }11 });12})
Rendering Exceptions as JSON
When rendering an exception, Laravel will automatically determine if the exception should be rendered as an HTML or JSON response based on the Accept
header of the request. If you would like to customize how Laravel determines whether to render HTML or JSON exception responses, you may utilize the shouldRenderJsonWhen
method:
1use Illuminate\Http\Request;2use Throwable;34->withExceptions(function (Exceptions $exceptions) {5 $exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $e) {6 if ($request->is('admin/*')) {7 return true;8 }910 return $request->expectsJson();11 });12})
1use Illuminate\Http\Request;2use Throwable;34->withExceptions(function (Exceptions $exceptions) {5 $exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $e) {6 if ($request->is('admin/*')) {7 return true;8 }910 return $request->expectsJson();11 });12})
Customizing the Exception Response
Rarely, you may need to customize the entire HTTP response rendered by Laravel's exception handler. To accomplish this, you may register a response customization closure using the respond
method:
1use Symfony\Component\HttpFoundation\Response;23->withExceptions(function (Exceptions $exceptions) {4 $exceptions->respond(function (Response $response) {5 if ($response->getStatusCode() === 419) {6 return back()->with([7 'message' => 'The page expired, please try again.',8 ]);9 }1011 return $response;12 });13})
1use Symfony\Component\HttpFoundation\Response;23->withExceptions(function (Exceptions $exceptions) {4 $exceptions->respond(function (Response $response) {5 if ($response->getStatusCode() === 419) {6 return back()->with([7 'message' => 'The page expired, please try again.',8 ]);9 }1011 return $response;12 });13})
Reportable and Renderable Exceptions
Instead of defining custom reporting and rendering behavior in your application's bootstrap/app.php
file, you may define report
and render
methods directly on your application's exceptions. When these methods exist, they will automatically be called by the framework:
1<?php23namespace App\Exceptions;45use Exception;6use Illuminate\Http\Request;7use Illuminate\Http\Response;89class InvalidOrderException extends Exception10{11 /**12 * Report the exception.13 */14 public function report(): void15 {16 // ...17 }1819 /**20 * Render the exception into an HTTP response.21 */22 public function render(Request $request): Response23 {24 return response(/* ... */);25 }26}
1<?php23namespace App\Exceptions;45use Exception;6use Illuminate\Http\Request;7use Illuminate\Http\Response;89class InvalidOrderException extends Exception10{11 /**12 * Report the exception.13 */14 public function report(): void15 {16 // ...17 }1819 /**20 * Render the exception into an HTTP response.21 */22 public function render(Request $request): Response23 {24 return response(/* ... */);25 }26}
若你的 Exception 繼承的 Exception 已經是可轉譯的了 (如 Laravel 或 Symfony 內建的 Exception),可在該 Exception 的 render
方法內回傳 false
來轉譯某個 Exception 的預設 HTTP Response:
1/**2 * Render the exception into an HTTP response.3 */4public function render(Request $request): Response|bool5{6 if (/** Determine if the exception needs custom rendering */) {78 return response(/* ... */);9 }1011 return false;12}
1/**2 * Render the exception into an HTTP response.3 */4public function render(Request $request): Response|bool5{6 if (/** Determine if the exception needs custom rendering */) {78 return response(/* ... */);9 }1011 return false;12}
若你的 Exception 中包含了只有在特定情況下才會使用的自訂回報邏輯,則可讓 Laravel 在某些時候使用預設的 Exception 處理設定來回報這個 Exception。若要這麼做,請在該 Exception 的 report
方法內回傳 false
:
1/**2 * Report the exception.3 */4public function report(): bool5{6 if (/** Determine if the exception needs custom reporting */) {78 // ...910 return true;11 }1213 return false;14}
1/**2 * Report the exception.3 */4public function report(): bool5{6 if (/** Determine if the exception needs custom reporting */) {78 // ...910 return true;11 }1213 return false;14}
可以在 report
方法中型別提示任何的相依性。Laravel 的 Service Container 會自動插入這些相依性。
頻率限制回報的 Exception
若你的專案會回報大量的 Exception,則你可能會想針對實際要被 Log 與傳送到專案外部錯誤追蹤服務的 Exception 進行頻率限制。
To take a random sample rate of exceptions, you may use the throttle
exception method in your application's bootstrap/app.php
file. The throttle
method receives a closure that should return a Lottery
instance:
1use Illuminate\Support\Lottery;2use Throwable;34->withExceptions(function (Exceptions $exceptions) {5 $exceptions->throttle(function (Throwable $e) {6 return Lottery::odds(1, 1000);7 });8})
1use Illuminate\Support\Lottery;2use Throwable;34->withExceptions(function (Exceptions $exceptions) {5 $exceptions->throttle(function (Throwable $e) {6 return Lottery::odds(1, 1000);7 });8})
也可以根據 Exception 的型別來有條件地採樣。若只想採樣特定 Exception 類別的實體,只需要針對該類別回傳 Lottery
實體即可:
1use App\Exceptions\ApiMonitoringException;2use Illuminate\Support\Lottery;3use Throwable;45->withExceptions(function (Exceptions $exceptions) {6 $exceptions->throttle(function (Throwable $e) {7 if ($e instanceof ApiMonitoringException) {8 return Lottery::odds(1, 1000);9 }10 });11})
1use App\Exceptions\ApiMonitoringException;2use Illuminate\Support\Lottery;3use Throwable;45->withExceptions(function (Exceptions $exceptions) {6 $exceptions->throttle(function (Throwable $e) {7 if ($e instanceof ApiMonitoringException) {8 return Lottery::odds(1, 1000);9 }10 });11})
若不回傳 Lottery
而回傳 Limit
實體的話,就可以針對 Exception 的 Log 或傳送到外部錯誤追蹤服務進行頻率限制。這麼做可以避免突然增加的 Exception 使 Log 暴增,例如當網站使用的第三方服務突然離線的情況:
1use Illuminate\Broadcasting\BroadcastException;2use Illuminate\Cache\RateLimiting\Limit;3use Throwable;45->withExceptions(function (Exceptions $exceptions) {6 $exceptions->throttle(function (Throwable $e) {7 if ($e instanceof BroadcastException) {8 return Limit::perMinute(300);9 }10 });11})
1use Illuminate\Broadcasting\BroadcastException;2use Illuminate\Cache\RateLimiting\Limit;3use Throwable;45->withExceptions(function (Exceptions $exceptions) {6 $exceptions->throttle(function (Throwable $e) {7 if ($e instanceof BroadcastException) {8 return Limit::perMinute(300);9 }10 });11})
預設情況下,會使用 Exception 的類別名稱來作為頻率限制的索引鍵。可以在 Limit
上使用 by
方法來指定自定的索引鍵:
1use Illuminate\Broadcasting\BroadcastException;2use Illuminate\Cache\RateLimiting\Limit;3use Throwable;45->withExceptions(function (Exceptions $exceptions) {6 $exceptions->throttle(function (Throwable $e) {7 if ($e instanceof BroadcastException) {8 return Limit::perMinute(300)->by($e->getMessage());9 }10 });11})
1use Illuminate\Broadcasting\BroadcastException;2use Illuminate\Cache\RateLimiting\Limit;3use Throwable;45->withExceptions(function (Exceptions $exceptions) {6 $exceptions->throttle(function (Throwable $e) {7 if ($e instanceof BroadcastException) {8 return Limit::perMinute(300)->by($e->getMessage());9 }10 });11})
當然,可以在不同的 Exception 間混合使用 Lottery
與 Limit
實體:
1use App\Exceptions\ApiMonitoringException;2use Illuminate\Broadcasting\BroadcastException;3use Illuminate\Cache\RateLimiting\Limit;4use Illuminate\Support\Lottery;5use Throwable;67->withExceptions(function (Exceptions $exceptions) {8 $exceptions->throttle(function (Throwable $e) {9 return match (true) {10 $e instanceof BroadcastException => Limit::perMinute(300),11 $e instanceof ApiMonitoringException => Lottery::odds(1, 1000),12 default => Limit::none(),13 };14 });15})
1use App\Exceptions\ApiMonitoringException;2use Illuminate\Broadcasting\BroadcastException;3use Illuminate\Cache\RateLimiting\Limit;4use Illuminate\Support\Lottery;5use Throwable;67->withExceptions(function (Exceptions $exceptions) {8 $exceptions->throttle(function (Throwable $e) {9 return match (true) {10 $e instanceof BroadcastException => Limit::perMinute(300),11 $e instanceof ApiMonitoringException => Lottery::odds(1, 1000),12 default => Limit::none(),13 };14 });15})
HTTP Exception
有的 Exception 是用來描述伺服器的 HTTP 錯誤代碼。例如,這些 Exception 可能是:「找不到頁面」錯誤 (404)、「未經授權」錯誤 (401) 等,甚至是開發人員造成的 500 錯誤。在你的程式中的任何地點內,若要產生這種 Response,可使用 abort
輔助函式:
1abort(404);
1abort(404);
自訂 HTTP 錯誤頁面
在 Laravel 中,要給各種 HTTP 狀態碼顯示自訂錯誤頁非常容易。舉例來說,若要自訂 404 HTTP 狀態碼的錯誤頁面,請建立 resources/views/errors/404.blade.php
View 樣板。程式中只要產生 404 錯誤,就會轉譯這個 View。在該目錄中的 View 應以對應的 HTTP 狀態碼來命名。由 abort
函式產生的 Symfony\Component\HttpKernel\Exception\HttpException
實體會以 $exception
變數傳給該 View:
1<h2>{{ $exception->getMessage() }}</h2>
1<h2>{{ $exception->getMessage() }}</h2>
可以使用 vendor:publish
Artisan 指令來將 Laravel 的預設錯誤頁樣板安裝到專案內。安裝好樣板後,就可以隨意自訂這些樣板:
1php artisan vendor:publish --tag=laravel-errors
1php artisan vendor:publish --tag=laravel-errors
遞補的 HTTP 錯誤頁
可以為給定的一系列 HTTP 狀態碼定義一個「遞補的」錯誤頁面。當發生的 HTTP 狀態碼沒有對應頁面時,就會轉譯這個遞補的頁面。若要使用遞補頁面,請在專案的 resources/views/errors
目錄下定義一個 4xx.blade.php
樣板與 5xx.blade.php
樣板。
When defining fallback error pages, the fallback pages will not affect 404
, 500
, and 503
error responses since Laravel has internal, dedicated pages for these status codes. To customize the pages rendered for these status codes, you should define a custom error page for each of them individually.