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

# Cliente HTTP

> Aprende a enviar peticiones a APIs externas con el cliente HTTP de Laravel. Se cubren autenticación, timeouts, reintentos y pruebas.

## Qué es el cliente HTTP

El cliente HTTP de Laravel es una API sencilla que envuelve a [Guzzle](https://docs.guzzlephp.org/en/stable/).
A través del facade `Http` puedes escribir con concisión peticiones HTTP a servicios y APIs externas.

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

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

<Info>
  Guzzle viene preinstalado, así que puedes empezar a usar el cliente sin configuración adicional.
</Info>

## Peticiones básicas

### Petición GET

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

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

Los parámetros de consulta se pasan como array.

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

### Petición POST

Los datos se envían por defecto como `application/json`.

```php theme={null}
$response = Http::post('https://api.example.com/users', [
    'name' => 'Ana García',
    'email' => 'ana@example.com',
]);
```

### PUT / PATCH / DELETE

```php theme={null}
// Petición PUT
$response = Http::put('https://api.example.com/users/1', [
    'name' => 'Ana López',
]);

// Petición PATCH
$response = Http::patch('https://api.example.com/users/1', [
    'email' => 'anal@example.com',
]);

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

## Manejo de la respuesta

Métodos como `Http::get()` devuelven una instancia de `Illuminate\Http\Client\Response`.
El objeto ofrece numerosos métodos para inspeccionar la respuesta.

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

// Cuerpo de la respuesta
$response->body();        // Como cadena
$response->json();        // Convertido en array
$response->json('name');  // Una clave concreta
$response->object();      // Como objeto stdClass
$response->collect();     // Como Collection

// Estado
$response->status();      // Código HTTP (p. ej. 200)
$response->successful();  // true si es 2xx
$response->failed();      // true si es 4xx o 5xx
$response->clientError(); // true si es 4xx
$response->serverError(); // true si es 5xx

// Códigos habituales
$response->ok();           // 200
$response->created();      // 201
$response->noContent();    // 204
$response->notFound();     // 404
$response->unauthorized(); // 401
$response->forbidden();    // 403
$response->unprocessableEntity(); // 422
$response->tooManyRequests();     // 429
```

Las respuestas JSON también son accesibles como array.

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

## Opciones de la petición

### Cabeceras

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

Si quieres indicar que aceptas `application/json`, `acceptJson()` es una forma más cómoda.

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

<Info>
  Para las cabeceras CSRF (`X-CSRF-TOKEN` / `X-XSRF-TOKEN`) en peticiones AJAX del navegador hacia Laravel, consulta [protección CSRF](/es/csrf).
</Info>

### Autenticación

**Autenticación con Bearer token** (la más habitual):

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

**Autenticación básica**:

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

### URL base

Si haces muchas peticiones al mismo host, agrúpalas con `baseUrl()`.

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

### Enviar datos de formulario

Para enviar con `application/x-www-form-urlencoded` utiliza `asForm()`.

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

### Timeouts

```php theme={null}
// Timeout de respuesta (por defecto 30 s)
$response = Http::timeout(10)->get('https://api.example.com/slow-endpoint');

// Timeout de conexión (por defecto 10 s)
$response = Http::connectTimeout(5)->get('https://api.example.com/endpoint');
```

<Warning>
  Si se supera el timeout se lanza `Illuminate\Http\Client\ConnectionException`.
  Es recomendable configurar siempre un timeout en las llamadas a APIs externas.
</Warning>

### Reintentos

Puedes configurar reintentos automáticos ante errores puntuales de red o del servidor.

```php theme={null}
// Hasta 3 reintentos con 100 ms de intervalo
$response = Http::retry(3, 100)->post('https://api.example.com/orders', $data);
```

Reintentos condicionales (por ejemplo, solo cuando haya error de conexión):

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

## Gestión de errores

### Comprobar los errores manualmente

Por defecto, el cliente HTTP de Laravel no lanza excepciones ante respuestas 4xx o 5xx.
Compruébalas explícitamente con `failed()`, `clientError()`, etc.

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

if ($response->notFound()) {
    // Manejar el 404
}

if ($response->failed()) {
    // Registrar el error, etc.
    logger()->error('API request failed', ['status' => $response->status()]);
}
```

### Lanzar excepciones

Con `throw()` se lanza `Illuminate\Http\Client\RequestException` cuando hay errores.

```php theme={null}
// Lanza excepción si la respuesta es 4xx/5xx
$response = Http::post('https://api.example.com/users', $data)->throw();

// throwIf(): lanza excepción cuando la condición es true (ejemplo independiente)
$response = Http::get('https://api.example.com/users/1');
$response->throwIf($response->status() === 422);

// throwUnlessStatus(): lanza excepción salvo con el código indicado (ejemplo independiente)
$response = Http::post('https://api.example.com/orders', $data);
$response->throwUnlessStatus(201);
```

`throw()` devuelve la propia respuesta, por lo que se puede encadenar.

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

Ejemplo de captura de excepciones:

```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) {
    // Timeout o error de conexión
    logger()->error('Connection failed: ' . $e->getMessage());
} catch (RequestException $e) {
    // Error 4xx / 5xx
    logger()->error('API error', ['status' => $e->response->status()]);
}
```

## Peticiones concurrentes

Cuando necesites llamar a varias APIs simultáneamente, utiliza `pool()` para ejecutarlas en paralelo.

```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>
  Es mucho más rápido que hacerlas en secuencia. Útil en paneles que consumen varias APIs externas.
</Tip>

## Pruebas

### Mock con `Http::fake()`

En tests usa `Http::fake()` para simular respuestas sin realizar la petición real.

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

Http::fake();

// Todas las peticiones devuelven 200
$response = Http::get('https://api.example.com/users');
$response->successful(); // true
```

Definir respuestas para URLs concretas:

```php theme={null}
Http::fake([
    'api.example.com/users/*' => Http::response(['id' => 1, 'name' => 'Ana García'], 200),
    'api.example.com/posts/*' => Http::response(['error' => 'Not Found'], 404),
    '*' => Http::response('OK', 200),  // El resto
]);
```

Secuencia de respuestas (devuelve una respuesta distinta en cada invocación):

```php theme={null}
Http::fake([
    // 1ª: 200, 2ª: 200, 3ª: 429
    // Cuando se agota la secuencia las peticiones posteriores lanzan excepción
    'api.example.com/*' => Http::sequence()
        ->push(['id' => 1], 200)
        ->push(['id' => 2], 200)
        ->pushStatus(429),
]);
```

### Aserciones sobre la petición

Con `Http::assertSent()` puedes validar el contenido de las peticiones enviadas.

```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' => 'Ana García',
]);

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

// Comprobar que no se ha enviado
Http::assertNotSent(function (Request $request) {
    return $request->url() === 'https://api.example.com/admin';
});

// Comprobar que se ha enviado exactamente una petición
Http::assertSentCount(1);
```

<Tip>
  En los tests, invoca siempre `Http::fake()` al principio.
  Si te lo saltas, las peticiones irán a las APIs reales.
  Con `Http::preventStrayRequests()` puedes hacer que las peticiones a URLs no simuladas lancen una excepción.
</Tip>

### Prevención de peticiones no simuladas

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

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

// Las peticiones a URLs no simuladas lanzan excepción
Http::get('https://other.example.com/endpoint'); // Excepción
```

## Ejemplo práctico: clase de servicio para una API externa

En proyectos reales, la buena práctica es agrupar la lógica del cliente HTTP en clases de servicio.

<Steps>
  <Step title="Crear la clase de servicio">
    ```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,
        ) {}

        /**
         * Obtiene información de un usuario
         *
         * @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();
        }

        /**
         * Obtiene la lista de repositorios
         *
         * @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="Registrar el servicio en el service provider">
    ```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="Usarlo desde el controlador">
    ```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' => 'No se ha podido conectar con la API de GitHub.'], 503);
            } catch (RequestException $e) {
                $status = $e->response->status();
                if ($status === 404) {
                    return response()->json(['error' => 'Usuario no encontrado.'], 404);
                }
                return response()->json(['error' => 'Se ha producido un error en la API de GitHub.'], 502);
            }
        }
    }
    ```
  </Step>

  <Step title="Escribir los tests">
    ```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_obtiene_la_informacion_del_usuario(): 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_lanza_excepcion_con_usuario_inexistente(): void
        {
            $this->expectException(RequestException::class);

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

## Resumen

<AccordionGroup>
  <Accordion title="Métodos más habituales">
    | Método                     | Uso                           |
    | -------------------------- | ----------------------------- |
    | `Http::get($url, $query)`  | Petición GET                  |
    | `Http::post($url, $data)`  | Petición POST (JSON)          |
    | `Http::put($url, $data)`   | Petición PUT                  |
    | `Http::patch($url, $data)` | Petición PATCH                |
    | `Http::delete($url)`       | Petición DELETE               |
    | `->withToken($token)`      | Autenticación Bearer          |
    | `->withHeaders($headers)`  | Cabeceras personalizadas      |
    | `->timeout($seconds)`      | Configuración de timeout      |
    | `->retry($times, $sleep)`  | Reintentos automáticos        |
    | `->throw()`                | Lanzar excepción si hay error |
    | `Http::fake()`             | Mock para tests               |
    | `Http::pool($callback)`    | Peticiones concurrentes       |
  </Accordion>

  <Accordion title="Métodos de comprobación de la respuesta">
    | Método                  | Descripción |
    | ----------------------- | ----------- |
    | `successful()`          | 2xx         |
    | `failed()`              | 4xx o 5xx   |
    | `clientError()`         | 4xx         |
    | `serverError()`         | 5xx         |
    | `ok()`                  | 200         |
    | `created()`             | 201         |
    | `notFound()`            | 404         |
    | `unauthorized()`        | 401         |
    | `forbidden()`           | 403         |
    | `unprocessableEntity()` | 422         |
    | `tooManyRequests()`     | 429         |
  </Accordion>

  <Accordion title="Buenas prácticas al integrar APIs externas">
    * Agrupa la lógica del cliente HTTP en clases de servicio.
    * Configura siempre un timeout (`timeout()` y `connectTimeout()`).
    * Configura reintentos para errores puntuales (`retry()`).
    * En los tests utiliza siempre `Http::fake()` y no llames nunca a las APIs reales.
    * Añadir `Http::preventStrayRequests()` al setup te da un extra de tranquilidad.
    * Guarda tokens y credenciales en variables de entorno y en `config/services.php`.
  </Accordion>
</AccordionGroup>


## Related topics

- [BlueskyManager y HasShortHand](/es/packages/laravel-bluesky/bluesky-manager.md)
- [Actualización de Laravel 8 a 9](/es/blog/upgrade-8-to-9.md)
- [Laravel Telescope](/es/telescope.md)
- [Modo nativo: Song (síntesis de voz cantada) - VOICEVOX for Laravel](/es/packages/laravel-voicevox/native-song.md)
- [Técnicas prácticas de Laravel Telescope](/es/blog/telescope-introduction.md)
