> ## 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` 파사드를 통해 외부의 웹 서비스나 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();  // 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 보호](/ko/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 클라이언트는 기본적으로는 400·500번대의 응답에 대해 예외를 스로우하지 않습니다.
`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()`를 사용하면 페이크하지 않은 URL로의 요청에서 예외를 스로우시킬 수 있습니다.
</Tip>

### Stray 요청의 방지

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

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

// 페이크되지 않은 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()`          | 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 클라이언트의 로직은 서비스 클래스에 정리
    * 반드시 타임아웃을 설정(`timeout()`과 `connectTimeout()`)
    * 일시적인 장애에 대해 리트라이를 설정(`retry()`)
    * 테스트에서는 `Http::fake()`를 반드시 사용해 외부 API를 실제로 두드리지 않음
    * `Http::preventStrayRequests()`를 테스트의 셋업에 추가하면 안심
    * API 의 토큰이나 인증 정보는 환경 변수와 `config/services.php` 로 관리
  </Accordion>
</AccordionGroup>


## Related topics

- [BlueskyManager와 HasShortHand](/ko/packages/laravel-bluesky/bluesky-manager.md)
- [클라이언트 모드: 토크 (텍스트 음성 합성) - VOICEVOX for Laravel](/ko/packages/laravel-voicevox/client-talk.md)
- [클라이언트 모드 - 프리셋 - VOICEVOX for Laravel](/ko/packages/laravel-voicevox/client-presets.md)
- [클라이언트 모드 - 사용자 사전 - VOICEVOX for Laravel](/ko/packages/laravel-voicevox/client-user-dict.md)
- [클라이언트 모드: 송 (노래 음성 합성) - VOICEVOX for Laravel](/ko/packages/laravel-voicevox/client-song.md)
