> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# HTTP 客户端

> 介绍如何使用 Laravel 的 HTTP 客户端向外部 API 发起请求。涵盖鉴权、超时、重试与测试。

## 什么是 HTTP 客户端

Laravel 的 HTTP 客户端是对 [Guzzle](https://docs.guzzlephp.org/en/stable/) 的封装，提供了一套易用的 API。
通过 `Http` Facade，可以简洁地写出对外部 Web 服务或 API 的 HTTP 请求。

```php theme={null}
use Illuminate\Support\Facades\Http;

$response = Http::get('https://api.example.com/users');
```

<Info>
  Guzzle 已经预装，无需额外配置即可开始使用。
</Info>

## 基础请求

### GET 请求

```php theme={null}
use Illuminate\Support\Facades\Http;

$response = Http::get('https://api.example.com/users');
```

查询参数可以通过数组传入。

```php theme={null}
$response = Http::get('https://api.example.com/users', [
    'page' => 1,
    'per_page' => 20,
]);
```

### POST 请求

数据默认作为 `application/json` 发送。

```php theme={null}
$response = Http::post('https://api.example.com/users', [
    'name' => '山田 太郎',
    'email' => 'taro@example.com',
]);
```

### PUT / PATCH / DELETE

```php theme={null}
// PUT 请求
$response = Http::put('https://api.example.com/users/1', [
    'name' => '山田 花子',
]);

// PATCH 请求
$response = Http::patch('https://api.example.com/users/1', [
    'email' => 'hanako@example.com',
]);

// DELETE 请求
$response = Http::delete('https://api.example.com/users/1');
```

## 处理响应

`Http::get()` 等方法会返回一个 `Illuminate\Http\Client\Response` 实例。
在该对象上有大量用于检查响应的方法。

```php theme={null}
$response = Http::get('https://api.example.com/users/1');

// 响应体
$response->body();        // 以字符串形式获取
$response->json();        // 解析为数组
$response->json('name');  // 获取 JSON 中的指定键
$response->object();      // 以 stdClass 对象形式获取
$response->collect();     // 以 Collection 形式获取

// 状态
$response->status();      // 状态码（如 200）
$response->successful();  // 2xx 时返回 true
$response->failed();      // 4xx 及以上返回 true
$response->clientError(); // 4xx 时返回 true
$response->serverError(); // 5xx 时返回 true

// 常用状态码判定
$response->ok();           // 200
$response->created();      // 201
$response->noContent();    // 204
$response->notFound();     // 404
$response->unauthorized(); // 401
$response->forbidden();    // 403
$response->unprocessableEntity(); // 422
$response->tooManyRequests();     // 429
```

JSON 响应也可以通过数组访问方式取值。

```php theme={null}
$name = Http::get('https://api.example.com/users/1')['name'];
```

## 请求选项

### 设置请求头

```php theme={null}
$response = Http::withHeaders([
    'X-Api-Version' => '2',
    'Accept-Language' => 'zh',
])->get('https://api.example.com/users');
```

若要声明期望接收 `application/json`，可以使用 `acceptJson()`。

```php theme={null}
$response = Http::acceptJson()->get('https://api.example.com/users');
```

<Info>
  浏览器向 Laravel 发送 AJAX 请求时的 CSRF 头（`X-CSRF-TOKEN` / `X-XSRF-TOKEN`）可参考 [CSRF 保护](/zh/csrf)。
</Info>

### 认证

**Bearer 令牌认证**（最常见）：

```php theme={null}
$response = Http::withToken($token)->get('https://api.example.com/me');
```

**Basic 认证**：

```php theme={null}
$response = Http::withBasicAuth('user@example.com', 'password')
    ->get('https://api.example.com/private');
```

### 设置基础 URL

如果对同一主机的请求较多，可以用 `baseUrl()` 统一设置。

```php theme={null}
$response = Http::baseUrl('https://api.example.com')
    ->withToken($token)
    ->get('/users/1');
```

### 发送表单数据

要以 `application/x-www-form-urlencoded` 发送，使用 `asForm()`。

```php theme={null}
$response = Http::asForm()->post('https://api.example.com/login', [
    'username' => 'taro',
    'password' => 'secret',
]);
```

### 超时

```php theme={null}
// 响应超时（默认 30 秒）
$response = Http::timeout(10)->get('https://api.example.com/slow-endpoint');

// 连接超时（默认 10 秒）
$response = Http::connectTimeout(5)->get('https://api.example.com/endpoint');
```

<Warning>
  超时会抛出 `Illuminate\Http\Client\ConnectionException`。
  建议在调用外部 API 时始终设置超时。
</Warning>

### 重试

可以为临时性的网络故障或服务器错误设置自动重试。

```php theme={null}
// 最多重试 3 次，每次间隔 100 毫秒
$response = Http::retry(3, 100)->post('https://api.example.com/orders', $data);
```

条件重试（例如仅在连接错误时重试）：

```php theme={null}
use Illuminate\Http\Client\PendingRequest;
use Throwable;
use Illuminate\Http\Client\ConnectionException;

$response = Http::retry(3, 100, function (Throwable $exception, PendingRequest $request) {
    return $exception instanceof ConnectionException;
})->post('https://api.example.com/orders', $data);
```

## 错误处理

### 手动检查错误

Laravel 的 HTTP 客户端默认不会对 4xx、5xx 响应抛出异常。
需要通过 `failed()`、`clientError()` 等方法显式判断。

```php theme={null}
$response = Http::get('https://api.example.com/users/999');

if ($response->notFound()) {
    // 处理 404
}

if ($response->failed()) {
    // 记录错误日志
    logger()->error('API request failed', ['status' => $response->status()]);
}
```

### 抛出异常

使用 `throw()` 时，遇到错误就会抛出 `Illuminate\Http\Client\RequestException`。

```php theme={null}
// 遇到错误响应（4xx/5xx）时抛出异常
$response = Http::post('https://api.example.com/users', $data)->throw();

// throwIf(): 当条件为 true 时抛出（以下是独立示例）
$response = Http::get('https://api.example.com/users/1');
$response->throwIf($response->status() === 422);

// throwUnlessStatus(): 只要不是指定状态码就抛出（以下也是独立示例）
$response = Http::post('https://api.example.com/orders', $data);
$response->throwUnlessStatus(201);
```

`throw()` 会返回响应实例，因此可以链式调用。

```php theme={null}
$user = Http::post('https://api.example.com/users', $data)
    ->throw()
    ->json();
```

若要捕获异常处理：

```php theme={null}
use Illuminate\Http\Client\RequestException;
use Illuminate\Http\Client\ConnectionException;

try {
    $response = Http::timeout(5)
        ->post('https://api.example.com/users', $data)
        ->throw();
} catch (ConnectionException $e) {
    // 超时或连接错误
    logger()->error('Connection failed: ' . $e->getMessage());
} catch (RequestException $e) {
    // 4xx / 5xx 错误
    logger()->error('API error', ['status' => $e->response->status()]);
}
```

## 并发请求

如果需要同时调用多个 API，可以通过 `pool()` 并发执行。

```php theme={null}
use Illuminate\Http\Client\Pool;
use Illuminate\Support\Facades\Http;

$responses = Http::pool(fn (Pool $pool) => [
    $pool->as('users')->get('https://api.example.com/users'),
    $pool->as('posts')->get('https://api.example.com/posts'),
    $pool->as('comments')->get('https://api.example.com/comments'),
]);

$users    = $responses['users']->json();
$posts    = $responses['posts']->json();
$comments = $responses['comments']->json();
```

<Tip>
  比顺序执行明显更快，非常适合需要同时调用多个外部 API 的仪表盘等场景。
</Tip>

## 测试

### 使用 Http::fake() 模拟

测试中使用 `Http::fake()` 可以在不发起真实 HTTP 请求的情况下模拟响应。

```php theme={null}
use Illuminate\Support\Facades\Http;

Http::fake();

// 所有请求都返回 200
$response = Http::get('https://api.example.com/users');
$response->successful(); // true
```

针对特定 URL 设置响应：

```php theme={null}
Http::fake([
    'api.example.com/users/*' => Http::response(['id' => 1, 'name' => '山田 太郎'], 200),
    'api.example.com/posts/*' => Http::response(['error' => 'Not Found'], 404),
    '*' => Http::response('OK', 200),  // 其他所有请求
]);
```

响应序列（多次调用时按顺序返回）：

```php theme={null}
Http::fake([
    // 第 1 次: 200，第 2 次: 200，第 3 次: 429
    // 序列耗尽后再发请求会抛出异常
    'api.example.com/*' => Http::sequence()
        ->push(['id' => 1], 200)
        ->push(['id' => 2], 200)
        ->pushStatus(429),
]);
```

### 校验请求

`Http::assertSent()` 可以校验发出的请求内容。

```php theme={null}
use Illuminate\Http\Client\Request;
use Illuminate\Support\Facades\Http;

Http::fake();

Http::withToken('my-token')->post('https://api.example.com/users', [
    'name' => '山田 太郎',
]);

Http::assertSent(function (Request $request) {
    return $request->url() === 'https://api.example.com/users'
        && $request->hasHeader('Authorization', 'Bearer my-token')
        && $request['name'] === '山田 太郎';
});

// 确认没有发送过某个请求
Http::assertNotSent(function (Request $request) {
    return $request->url() === 'https://api.example.com/admin';
});

// 确认只发送过 1 次请求
Http::assertSentCount(1);
```

<Tip>
  测试中一定要在最开始调用 `Http::fake()`。
  忘记调用可能会真的对外部 API 发起请求。
  使用 `Http::preventStrayRequests()` 可以让未被 fake 的 URL 请求抛出异常。
</Tip>

### 阻止“漏网”请求

```php theme={null}
Http::preventStrayRequests();

Http::fake([
    'api.example.com/*' => Http::response(['ok' => true]),
]);

// 未被 fake 的 URL 请求会抛异常
Http::get('https://other.example.com/endpoint'); // 抛出异常
```

## 实战示例：调用外部 API 的服务类

在真实项目中，把 HTTP 客户端逻辑封装到服务类中是最佳实践。

<Steps>
  <Step title="创建服务类">
    ```php theme={null}
    <?php

    namespace App\Services;

    use Illuminate\Http\Client\RequestException;
    use Illuminate\Http\Client\ConnectionException;
    use Illuminate\Support\Facades\Http;

    class GitHubService
    {
        private string $baseUrl = 'https://api.github.com';

        public function __construct(
            private readonly string $token,
        ) {}

        /**
         * 获取用户信息。
         *
         * @throws ConnectionException
         * @throws RequestException
         */
        public function getUser(string $username): array
        {
            return Http::baseUrl($this->baseUrl)
                ->withToken($this->token)
                ->acceptJson()
                ->timeout(10)
                ->get("/users/{$username}")
                ->throw()
                ->json();
        }

        /**
         * 获取仓库列表。
         *
         * @throws ConnectionException
         * @throws RequestException
         */
        public function getRepositories(string $username, int $page = 1): array
        {
            return Http::baseUrl($this->baseUrl)
                ->withToken($this->token)
                ->acceptJson()
                ->timeout(10)
                ->retry(2, 500)
                ->get("/users/{$username}/repos", [
                    'page' => $page,
                    'per_page' => 30,
                    'sort' => 'updated',
                ])
                ->throw()
                ->json();
        }
    }
    ```
  </Step>

  <Step title="在服务提供者中注册">
    ```php theme={null}
    // app/Providers/AppServiceProvider.php

    use App\Services\GitHubService;

    public function register(): void
    {
        $this->app->singleton(GitHubService::class, function () {
            return new GitHubService(
                token: config('services.github.token'),
            );
        });
    }
    ```

    ```php theme={null}
    // config/services.php
    'github' => [
        'token' => env('GITHUB_TOKEN'),
    ],
    ```
  </Step>

  <Step title="在控制器中使用">
    ```php theme={null}
    <?php

    namespace App\Http\Controllers;

    use App\Services\GitHubService;
    use Illuminate\Http\Client\RequestException;
    use Illuminate\Http\Client\ConnectionException;
    use Illuminate\Http\JsonResponse;

    class GitHubController extends Controller
    {
        public function __construct(
            private readonly GitHubService $github,
        ) {}

        public function show(string $username): JsonResponse
        {
            try {
                $user = $this->github->getUser($username);
                return response()->json($user);
            } catch (ConnectionException) {
                return response()->json(['error' => '无法连接到 GitHub API。'], 503);
            } catch (RequestException $e) {
                $status = $e->response->status();
                if ($status === 404) {
                    return response()->json(['error' => '用户不存在。'], 404);
                }
                return response()->json(['error' => 'GitHub API 出错。'], 502);
            }
        }
    }
    ```
  </Step>

  <Step title="编写测试">
    ```php theme={null}
    <?php

    namespace Tests\Unit\Services;

    use App\Services\GitHubService;
    use Illuminate\Http\Client\ConnectionException;
    use Illuminate\Http\Client\RequestException;
    use Illuminate\Support\Facades\Http;
    use Tests\TestCase;

    class GitHubServiceTest extends TestCase
    {
        private GitHubService $service;

        protected function setUp(): void
        {
            parent::setUp();

            Http::fake([
                'api.github.com/users/octocat' => Http::response([
                    'login' => 'octocat',
                    'name' => 'The Octocat',
                    'public_repos' => 8,
                ], 200),
                'api.github.com/users/notfound' => Http::response(
                    ['message' => 'Not Found'],
                    404
                ),
            ]);

            $this->service = new GitHubService(token: 'test-token');
        }

        public function test_可以获取用户信息(): void
        {
            $user = $this->service->getUser('octocat');

            $this->assertEquals('octocat', $user['login']);
            $this->assertEquals('The Octocat', $user['name']);

            Http::assertSent(function ($request) {
                return $request->url() === 'https://api.github.com/users/octocat'
                    && $request->hasHeader('Authorization', 'Bearer test-token');
            });
        }

        public function test_不存在的用户会抛出异常(): void
        {
            $this->expectException(RequestException::class);

            $this->service->getUser('notfound');
        }
    }
    ```
  </Step>
</Steps>

## 小结

<AccordionGroup>
  <Accordion title="常用方法速查">
    | 方法                         | 用途            |
    | -------------------------- | ------------- |
    | `Http::get($url, $query)`  | GET 请求        |
    | `Http::post($url, $data)`  | POST 请求（JSON） |
    | `Http::put($url, $data)`   | PUT 请求        |
    | `Http::patch($url, $data)` | PATCH 请求      |
    | `Http::delete($url)`       | DELETE 请求     |
    | `->withToken($token)`      | Bearer 认证     |
    | `->withHeaders($headers)`  | 自定义请求头        |
    | `->timeout($seconds)`      | 设置超时          |
    | `->retry($times, $sleep)`  | 自动重试          |
    | `->throw()`                | 错误时抛出异常       |
    | `Http::fake()`             | 用于测试的模拟       |
    | `Http::pool($callback)`    | 并发请求          |
  </Accordion>

  <Accordion title="响应判定方法速查">
    | 方法                      | 说明      |
    | ----------------------- | ------- |
    | `successful()`          | 2xx     |
    | `failed()`              | 4xx 及以上 |
    | `clientError()`         | 4xx     |
    | `serverError()`         | 5xx     |
    | `ok()`                  | 200     |
    | `created()`             | 201     |
    | `notFound()`            | 404     |
    | `unauthorized()`        | 401     |
    | `forbidden()`           | 403     |
    | `unprocessableEntity()` | 422     |
    | `tooManyRequests()`     | 429     |
  </Accordion>

  <Accordion title="外部 API 集成的最佳实践">
    * 将 HTTP 客户端逻辑封装到服务类中
    * 始终设置超时（`timeout()` 与 `connectTimeout()`）
    * 对临时故障设置自动重试（`retry()`）
    * 测试中一定使用 `Http::fake()`，不要真的访问外部 API
    * 在测试的 setUp 中加入 `Http::preventStrayRequests()` 更保险
    * API 令牌和凭证通过环境变量和 `config/services.php` 管理
  </Accordion>
</AccordionGroup>


## Related topics

- [BlueskyManager 与 HasShortHand](/zh/packages/laravel-bluesky/bluesky-manager.md)
- [客户端模式：Talk（文本语音合成） - VOICEVOX for Laravel](/zh/packages/laravel-voicevox/client-talk.md)
- [客户端模式 - 预设 - VOICEVOX for Laravel](/zh/packages/laravel-voicevox/client-presets.md)
- [客户端模式 - 用户词典 - VOICEVOX for Laravel](/zh/packages/laravel-voicevox/client-user-dict.md)
- [客户端模式：Song（歌声合成） - VOICEVOX for Laravel](/zh/packages/laravel-voicevox/client-song.md)
