
# 错误处理

- [简介](#introduction)
- [配置](#configuration)
- [处理异常](#handling-exceptions)
    - [报告异常](#reporting-exceptions)
    - [异常日志级别](#exception-log-levels)
    - [按类型忽略异常](#ignoring-exceptions-by-type)
    - [渲染异常](#rendering-exceptions)
    - [可报告与可渲染异常](#renderable-exceptions)
- [限制异常报告频率](#throttling-reported-exceptions)
- [HTTP 异常](#http-exceptions)
    - [自定义 HTTP 错误页面](#custom-http-error-pages)

## 简介

当你创建一个新的 Laravel 项目时，错误和异常处理已经为你配置好了；不过，你可以随时在应用程序的 `bootstrap/app.php` 文件中使用 `withExceptions` 方法，管理应用程序如何报告和渲染异常。

传递给 `withExceptions` 闭包的 `$exceptions` 对象是 `Illuminate\Foundation\Configuration\Exceptions` 的一个实例，负责管理应用程序中的异常处理。在本文档接下来的内容中，我们将进一步介绍这个对象。

## 配置

`config/app.php` 配置文件中的 `debug` 选项决定了实际向用户显示多少错误信息。默认情况下，该选项会遵循存储在 `.env` 文件中的 `APP_DEBUG` 环境变量的值。

在本地开发过程中，应将 `APP_DEBUG` 环境变量设置为 `true`。

> [!警告]
> 在生产环境中，`APP_DEBUG` 的值应始终设置为 `false`。如果在生产环境中将其设置为 `true`，可能会导致应用程序的敏感配置值暴露给最终用户。

## 处理异常

### 报告异常

在 Laravel 中，异常报告用于记录异常，或者将异常发送到诸如 [Sentry](https://github.com/getsentry/sentry-laravel) 或 [Flare](https://flareapp.io) 这样的外部服务。默认情况下，异常会根据你的[日志](/docs/laravel/13.x/logging)配置进行记录。不过，你也可以按照自己的需求，以任意方式记录异常。

如果你需要以不同的方式报告不同类型的异常，可以在应用程序的 `bootstrap/app.php` 文件中使用 `report` 异常方法注册一个闭包。当需要报告指定类型的异常时，该闭包就会被执行。Laravel 会通过检查闭包的类型提示来确定该闭包负责报告哪种类型的异常：

```php
use App\Exceptions\InvalidOrderException;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->report(function (InvalidOrderException $e) {
        // ...
    });
})
```

当你使用 `report` 方法注册自定义异常报告回调时，Laravel 仍然会使用应用程序默认的日志配置来记录该异常。如果你希望阻止异常继续传递到默认日志栈，可以在定义报告回调时使用 `stop` 方法，或者从回调中返回 `false`：

```php
use App\Exceptions\InvalidOrderException;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->report(function (InvalidOrderException $e) {
        // ...
    })->stop();

    $exceptions->report(function (InvalidOrderException $e) {
        return false;
    });
})
```

> [!注意]
> 如果要为指定异常自定义异常报告逻辑，也可以使用[可报告异常](/docs/laravel/13.x/errors#renderable-exceptions)。

#### 全局日志上下文

如果当前用户信息可用，Laravel 会自动将当前用户的 ID 作为上下文数据添加到每条异常日志消息中。你可以在应用程序的 `bootstrap/app.php` 文件中使用 `context` 异常方法定义自己的全局上下文数据。这些信息会包含在应用程序写入的每一条异常日志消息中：

```php
->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->context(fn () => [
        'foo' => 'bar',
    ]);
})
```



#### 异常日志上下文

虽然为每条日志消息添加上下文信息很有用，但有时某个特定的异常可能具有独特的上下文信息，你希望将其包含在日志中。通过在应用程序的某个异常类中定义 `context` 方法，你可以指定与该异常相关的任何数据，这些数据将被添加到异常的日志记录中：

```php
<?php

namespace App\Exceptions;

use Exception;

class InvalidOrderException extends Exception
{
    // ...

    /**
     * 获取异常的上下文信息。
     *
     * @return array<string, mixed>
     */
    public function context(): array
    {
        return ['order_id' => $this->orderId];
    }
}
```

#### `report` 辅助函数

有时你可能需要报告异常，但仍然继续处理当前请求。`report` 辅助函数允许你快速报告异常，而不会向用户显示错误页面：

```php
public function isValid(string $value): bool
{
    try {
        // 验证该值...
    } catch (Throwable $e) {
        report($e);

        return false;
    }
}
```

#### 避免重复报告异常

如果你在整个应用程序中使用 `report` 函数，偶尔可能会多次报告同一个异常，从而在日志中产生重复记录。

如果你想确保同一个异常实例只被报告一次，可以在应用程序的 `bootstrap/app.php` 文件中调用 `dontReportDuplicates` 异常方法：

```php
->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->dontReportDuplicates();
})
```

现在，当使用同一个异常实例调用 `report` 辅助函数时，只有第一次调用会被报告：

```php
$original = new RuntimeException('Whoops!');

report($original); // 已报告

try {
    throw $original;
} catch (Throwable $caught) {
    report($caught); // 已忽略
}

report($original); // 已忽略
report($caught); // 已忽略
```



### 异常日志级别

当消息被写入应用程序的日志时，这些消息会以指定的日志级别进行记录，该级别表示所记录消息的严重程度或重要性。

如上所述，即使你使用 `report` 方法注册了自定义异常报告回调，Laravel 仍然会使用应用程序的默认日志配置来记录异常。但是，由于日志级别有时会影响消息被记录到哪些日志通道，因此你可能希望为某些异常配置特定的日志级别。

为此，你可以在应用程序的 `bootstrap/app.php` 文件中使用 `level` 异常方法。该方法接收异常类型作为第一个参数，日志级别作为第二个参数：

```php
use PDOException;
use Psr\Log\LogLevel;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->level(PDOException::class, LogLevel::CRITICAL);
})
```

### 按类型忽略异常

在构建应用程序时，有些类型的异常你可能永远不希望报告。要忽略这些异常，可以在应用程序的 `bootstrap/app.php` 文件中使用 `dontReport` 异常方法。传递给该方法的任何异常类都不会被报告，但它们仍然可以具有自定义的渲染逻辑：

```php
use App\Exceptions\InvalidOrderException;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->dontReport([
        InvalidOrderException::class,
    ]);
})
```

或者，你也可以直接使用 `Illuminate\Contracts\Debug\ShouldntReport` 接口来“标记”异常类。当异常类实现该接口时，Laravel 的异常处理器将永远不会报告该异常：

```php
<?php

namespace App\Exceptions;

use Exception;
use Illuminate\Contracts\Debug\ShouldntReport;

class PodcastProcessingException extends Exception implements ShouldntReport
{
    //
}
```



如果你需要更精细地控制何时忽略某种特定类型的异常，可以向 `dontReportWhen` 方法传递一个闭包：

```php
use App\Exceptions\InvalidOrderException;
use Throwable;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->dontReportWhen(function (Throwable $e) {
        return $e instanceof PodcastProcessingException &&
               $e->reason() === 'Subscription expired';
    });
})
```

Laravel 内部已经默认忽略某些类型的错误，例如由 404 HTTP 错误、来源不匹配产生的 403 HTTP 响应，或者无效 CSRF 令牌产生的 419 HTTP 响应所引发的异常。如果你希望 Laravel 不再忽略某种特定类型的异常，可以在应用程序的 `bootstrap/app.php` 文件中使用 `stopIgnoring` 异常方法：

```php
use Symfony\Component\HttpKernel\Exception\HttpException;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->stopIgnoring(HttpException::class);
})
```

### 渲染异常

默认情况下，Laravel 的异常处理器会自动将异常转换为 HTTP 响应。不过，你也可以为指定类型的异常注册自定义渲染闭包。你可以通过在应用程序的 `bootstrap/app.php` 文件中使用 `render` 异常方法来实现。

传递给 `render` 方法的闭包应该返回一个 `Illuminate\Http\Response` 实例，该实例可以通过 `response` 辅助函数生成。Laravel 将通过检查闭包的类型提示来确定该闭包负责渲染哪种类型的异常：

```php
use App\Exceptions\InvalidOrderException;
use Illuminate\Http\Request;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->render(function (InvalidOrderException $e, Request $request) {
        return response()->view('errors.invalid-order', status: 500);
    });
})
```

你也可以使用 `render` 方法覆盖 Laravel 或 Symfony 内置异常（例如 `NotFoundHttpException`）的渲染行为。如果传递给 `render` 方法的闭包没有返回值，Laravel 将使用默认的异常渲染方式：

```php
use Illuminate\Http\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->render(function (NotFoundHttpException $e, Request $request) {
        if ($request->is('api/*')) {
            return response()->json([
                'message' => 'Record not found.'
            ], 404);
        }
    });
})
```



#### 将异常渲染为 JSON

在渲染异常时，Laravel 会根据请求的 `Accept` 头自动判断应该将异常渲染为 HTML 响应还是 JSON 响应。如果你想自定义 Laravel 判断异常响应应使用 HTML 还是 JSON 格式的方式，可以使用 `shouldRenderJsonWhen` 方法：

```php
use Illuminate\Http\Request;
use Throwable;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $e) {
        if ($request->is('admin/*')) {
            return true;
        }

        return $request->expectsJson();
    });
})
```

#### 自定义异常响应

在少数情况下，你可能需要自定义 Laravel 异常处理器渲染的整个 HTTP 响应。为此，你可以使用 `respond` 方法注册一个响应自定义闭包：

```php
use Symfony\Component\HttpFoundation\Response;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->respond(function (Response $response) {
        if ($response->getStatusCode() === 419) {
            return back()->with([
                'message' => 'The page expired, please try again.',
            ]);
        }

        return $response;
    });
})
```

### 可报告和可渲染的异常

除了在应用程序的 `bootstrap/app.php` 文件中定义自定义异常报告和渲染行为之外，你还可以直接在应用程序的异常类中定义 `report` 和 `render` 方法。当这些方法存在时，框架会自动调用它们：

```php
<?php

namespace App\Exceptions;

use Exception;
use Illuminate\Http\Request;
use Illuminate\Http\Response;

class InvalidOrderException extends Exception
{
    /**
     * 报告异常。
     */
    public function report(): void
    {
        // ...
    }

    /**
     * 将异常渲染为 HTTP 响应。
     */
    public function render(Request $request): Response
    {
        return response(/* ... */);
    }
}
```

如果你的异常继承自一个已经支持渲染的异常类，例如 Laravel 或 Symfony 的内置异常，你可以在异常的 `render` 方法中返回 `false`，以使用该异常默认的 HTTP 响应进行渲染：

```php
/**
 * 将异常渲染为 HTTP 响应。
 */
public function render(Request $request): Response|bool
{
    if (/** 判断异常是否需要自定义渲染 */) {

        return response(/* ... */);
    }

    return false;
}
```



如果你的异常包含仅在满足特定条件时才需要执行的自定义报告逻辑，你可能需要指示 Laravel 在某些情况下使用默认的异常处理配置来报告异常。为此，你可以在异常的 `report` 方法中返回 `false`：

```php
/**
 * 报告异常。
 */
public function report(): bool
{
    if (/** 判断异常是否需要自定义报告 */) {

        // ...

        return true;
    }

    return false;
}
```

> [!注意]
> 你可以对 `report` 方法所需的任何依赖项进行类型提示，Laravel 的[服务容器](/docs/laravel/13.x/container)会自动将这些依赖项注入到该方法中。


### 限制异常报告频率

如果你的应用程序报告了大量异常，你可能希望限制实际记录到日志或发送到应用程序外部错误跟踪服务的异常数量。

要对异常进行随机抽样，可以在应用程序的 `bootstrap/app.php` 文件中使用 `throttle` 异常方法。`throttle` 方法接收一个闭包，该闭包应返回一个 `Lottery` 实例：

```php
use Illuminate\Support\Lottery;
use Throwable;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->throttle(function (Throwable $e) {
        return Lottery::odds(1, 1000);
    });
})
```

你也可以根据异常类型有条件地进行抽样。如果你只想对特定异常类的实例进行抽样，可以仅针对该类返回一个 `Lottery` 实例：

```php
use App\Exceptions\ApiMonitoringException;
use Illuminate\Support\Lottery;
use Throwable;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->throttle(function (Throwable $e) {
        if ($e instanceof ApiMonitoringException) {
            return Lottery::odds(1, 1000);
        }
    });
})
```

你还可以通过返回 `Limit` 实例而不是 `Lottery` 实例，对记录到日志或发送到外部错误跟踪服务的异常进行速率限制。例如，当应用程序使用的第三方服务发生故障时，这可以防止异常突然大量涌入并淹没日志：

```php
use Illuminate\Broadcasting\BroadcastException;
use Illuminate\Cache\RateLimiting\Limit;
use Throwable;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->throttle(function (Throwable $e) {
        if ($e instanceof BroadcastException) {
            return Limit::perMinute(300);
        }
    });
})
```



默认情况下，速率限制会使用异常的类名作为限流键。你可以通过 `Limit` 的 `by` 方法指定自定义键来修改这一行为：

```php
use Illuminate\Broadcasting\BroadcastException;
use Illuminate\Cache\RateLimiting\Limit;
use Throwable;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->throttle(function (Throwable $e) {
        if ($e instanceof BroadcastException) {
            return Limit::perMinute(300)->by($e->getMessage());
        }
    });
})
```

当然，你也可以针对不同的异常返回 `Lottery` 和 `Limit` 实例的组合：

```php
use App\Exceptions\ApiMonitoringException;
use Illuminate\Broadcasting\BroadcastException;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Lottery;
use Throwable;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->throttle(function (Throwable $e) {
        return match (true) {
            $e instanceof BroadcastException => Limit::perMinute(300),
            $e instanceof ApiMonitoringException => Lottery::odds(1, 1000),
            default => Limit::none(),
        };
    });
})
```

## HTTP 异常

某些异常用于描述服务器返回的 HTTP 错误状态码。例如，“页面未找到”错误（404）、“未授权”错误（401），甚至是开发人员主动生成的 500 错误。要在应用程序的任何位置生成此类响应，可以使用 `abort` 辅助函数：

```php
abort(404);
```

### 自定义 HTTP 错误页面

Laravel 可以轻松地为各种 HTTP 状态码显示自定义错误页面。例如，要自定义 HTTP 404 状态码的错误页面，可以创建 `resources/views/errors/404.blade.php` 视图模板。该视图将用于渲染应用程序产生的所有 404 错误。此目录中的视图文件应以其对应的 HTTP 状态码命名。由 `abort` 函数抛出的 `Symfony\Component\HttpKernel\Exception\HttpException` 实例将作为 `$exception` 变量传递给视图：

```blade
<h2>{{ $exception->getMessage() }}</h2>
```



你可以使用 `vendor:publish` Artisan 命令发布 Laravel 的默认错误页面模板。发布模板后，你可以根据自己的需求对其进行自定义：

```shell
php artisan vendor:publish --tag=laravel-errors
```

#### 后备 HTTP 错误页面

你还可以为特定系列的 HTTP 状态码定义一个“后备”错误页面。如果发生的 HTTP 状态码没有对应的错误页面，就会渲染该后备页面。为此，可以在应用程序的 `resources/views/errors` 目录中定义 `4xx.blade.php` 和 `5xx.blade.php` 模板。

定义后备错误页面时，这些页面不会影响 `404`、`500` 和 `503` 错误响应，因为 Laravel 内部已经为这些状态码提供了专用页面。如果你想自定义这些状态码对应的错误页面，应该分别为它们定义自定义错误页面。

