> ## 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 特定 key
$response->object();      // stdClass 物件
$response->collect();     // 集合

// 狀態
$response->status();      // 狀態碼（例：200）
$response->successful();  // 200 系列時 true
$response->failed();      // 400 以上時 true
$response->clientError(); // 400 系列時 true
$response->serverError(); // 500 系列時 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' => 'ja',
])->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-TW/csrf)。
</Info>

### 認證

**Bearer token 認證**（最常見）：

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

### 設定 base 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() 進行 mock

測試時使用 `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';
});

// 確認送出的請求次數
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 的 Service 類別

實際專案中，將 HTTP 用戶端邏輯集中到 Service 類別是最佳實踐。

<Steps>
  <Step title="建立 Service 類別">
    ```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();
        }

        /**
         * 取得 repository 清單
         *
         * @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()`             | 測試用 mock      |
    | `Http::pool($callback)`    | 並行請求          |
  </Accordion>

  <Accordion title="回應判斷方法一覽">
    | 方法                      | 說明     |
    | ----------------------- | ------ |
    | `successful()`          | 200 系列 |
    | `failed()`              | 400 以上 |
    | `clientError()`         | 400 系列 |
    | `serverError()`         | 500 系列 |
    | `ok()`                  | 200    |
    | `created()`             | 201    |
    | `notFound()`            | 404    |
    | `unauthorized()`        | 401    |
    | `forbidden()`           | 403    |
    | `unprocessableEntity()` | 422    |
    | `tooManyRequests()`     | 429    |
  </Accordion>

  <Accordion title="與外部 API 整合的最佳實踐">
    * 將 HTTP 用戶端邏輯集中在 Service 類別
    * 一定要設定逾時（`timeout()` 與 `connectTimeout()`）
    * 為暫時性錯誤設定重試（`retry()`）
    * 測試務必使用 `Http::fake()`，不要真的打外部 API
    * 在測試 setUp 中加入 `Http::preventStrayRequests()` 更保險
    * API token 或認證資訊透過環境變數與 `config/services.php` 管理
  </Accordion>
</AccordionGroup>


## Related topics

- [Laravel MCP](/zh-TW/mcp.md)
- [Laravel Passkeys 初步調查（passkeys-server + @laravel/passkeys）](/zh-TW/blog/passkeys-introduction.md)
- [Laravel Passport（OAuth2 伺服器實作）](/zh-TW/passport.md)
- [2026 年 6 月 Laravel 更新](/zh-TW/blog/changelog/202606.md)
- [Laravel AI Agent 支援 MCP 伺服器](/zh-TW/blog/ai-sdk-mcp-client.md)
