> ## 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 y pruebas en paquetes

> Diseña clientes de API con la Factory HTTP compartida de Laravel 13 que aíslan el estado de cada solicitud, se registran como servicios y pueden verificarse con Http::fake().

Cuando un paquete necesita consumir una API externa, hay que conseguir dos cosas a la vez: **heredar la configuración del cliente HTTP que gestiona Laravel** y **no arrastrar la configuración de una llamada a la siguiente**. Si creas tu propia Factory o conservas durante mucho tiempo una solicitud antes de enviarla, los fakes de la aplicación que usa el paquete pueden dejar de aplicarse.

Esta página se basa en la revisión de la implementación de Laravel Framework `v13.39.0`. Para el uso básico, consulta el [cliente HTTP](https://kawax.biz/es/http-client.md), y para preparar el entorno de pruebas, [Pruebas de paquetes](https://kawax.biz/es/advanced/package-testing.md).

## Comparte la Factory y crea un PendingRequest en cada llamada

El cliente HTTP de Laravel incluye objetos con responsabilidades distintas.

| Objeto | Función | Uso en el paquete |
| - | - | - |
| `Illuminate\Http\Client\Factory` | Gestiona los fakes, las opciones comunes, los middleware comunes, etc. | Se recibe desde el contenedor de Laravel |
| `Illuminate\Http\Client\PendingRequest` | Contiene la configuración de la solicitud que se va a enviar: destino, cabeceras, tiempo de espera, etc. | Se crea cada vez que se llama a un método de la API |
| `Illuminate\Http\Client\Response` | Permite inspeccionar el código de estado y el cuerpo | Se trata como resultado de la comunicación y, si hace falta, se convierte en una excepción o un valor de retorno |

`FoundationServiceProvider` registra la Factory como singleton, y la fachada `Http` también la resuelve desde el contenedor con el mismo nombre de clase. Por eso, en una aplicación Laravel normal, **las solicitudes realizadas con la Factory recibida del contenedor también pueden verificarse con `Http::fake()`**. Es posible diseñar el código usando directamente la fachada, pero en esta página se inyecta la Factory para que las dependencias queden explícitas.

Los métodos habituales de configuración de solicitudes, como `baseUrl()`, que se llaman sobre la Factory se reenvían desde `__call()` a un nuevo PendingRequest. `createPendingRequest()` le pasa los callbacks de fake y la restricción de stray requests vigentes en el momento de crearlo, y `newPendingRequest()` aplica los middleware y las opciones comunes.

```mermaid theme={null}
flowchart LR
    A["Contenedor de Laravel"] --> B["Factory compartida"]
    C["Configurar respuestas con Http::fake()"] --> B
    B --> D["Nuevo PendingRequest<br>en cada llamada a la API"]
    D --> E["Configuración propia de la llamada"]
    E --> F["Envío e inspección de la respuesta"]
```

> [!WARNING]
>
> Si dentro del paquete creas `new Factory()` o un cliente de otra biblioteca HTTP, no se heredarán los fakes de la Factory compartida ni `Http::preventStrayRequests()`. Estas funciones no bloquean las comunicaciones de todo el proceso. En los clientes pensados para Laravel, usa la Factory compartida, y si ofreces un cliente independiente, proporciona un método de prueba específico para ese cliente.

## Agrupa las operaciones de la API en un cliente pequeño

Como ejemplo, usaremos el paquete `Acme\Catalog`, que obtiene productos de una API de catálogo ficticia. `src/CatalogClient.php` solo guarda la Factory y la URL base, y la solicitud se crea durante la ejecución de `product()`.

```php theme={null}
<?php

namespace Acme\Catalog;

use Illuminate\Http\Client\Factory;
use Illuminate\Http\Client\PendingRequest;
use UnexpectedValueException;

final class CatalogClient
{
    public function __construct(
        private readonly Factory $http,
        private readonly string $baseUrl,
    ) {
    }

    public function product(int $id, ?string $language = null): array
    {
        $request = $this->request();

        if ($language !== null) {
            $request->withHeaders(['Accept-Language' => $language]);
        }

        $data = $request->get('/products/'.$id)
            ->throw()
            ->json(flags: JSON_THROW_ON_ERROR);

        if (! is_array($data)) {
            throw new UnexpectedValueException('Catalog response must be an array.');
        }

        return $data;
    }

    private function request(): PendingRequest
    {
        return $this->http->baseUrl($this->baseUrl)
            ->acceptJson()
            ->connectTimeout(3)
            ->timeout(10);
    }
}
```

Con este diseño, aunque llames a `product(42, 'ja')` y después a `product(42)` con el mismo cliente, la cabecera `Accept-Language` añadida en la llamada anterior no se conserva. La configuración común que la aplicación haya definido en la Factory se sigue aplicando a cada solicitud.

* La configuración propia del paquete se aplica al PendingRequest recién creado. Evita configurar `Http::globalOptions()` durante la inicialización del paquete, porque afectaría también a comunicaciones que no tienen relación con él.
* No envíes nada en el constructor. Diseña el paquete para que resolver el servicio, iniciar Artisan o publicar el archivo de configuración no provoque conexiones a la API externa.
* `throw()` convierte las respuestas 4xx y 5xx en `RequestException`. Distínguelas de `ConnectionException`, que se produce por fallos de conexión o tiempos de espera agotados, y no sustituyas los fallos por valores de éxito como un array vacío.
* `json(flags: JSON_THROW_ON_ERROR)` convierte el JSON no válido en `JsonException`. El ejemplo solo comprueba si el resultado es un array, así que con una API real valida también los campos obligatorios y los tipos de datos.

> [!TIP]
>
> Si solo necesitas compartir la URL base o las cabeceras, las macros HTTP de la documentación oficial también son una opción. Si el paquete ofrece operaciones de API, la validación de los valores de retorno y un contrato de excepciones, separarlo en una clase dedicada como esta facilita el mantenimiento. Para el funcionamiento de las macros, consulta el [trait Macroable](https://kawax.biz/es/advanced/macroable.md).

## Registrar el cliente en el service provider

Prepara la configuración inicial en `config/catalog.php` del paquete. No guardes aquí clientes HTTP ni closures; incluye solo valores que puedan almacenarse en la caché de configuración.

```php theme={null}
<?php

return [
    'base_url' => 'https://catalog.example.test/api',
];
```

`src/CatalogServiceProvider.php` fusiona la configuración y pasa la Factory compartida al resolver el cliente. No sobrescribe el binding de la propia Factory.

```php theme={null}
<?php

namespace Acme\Catalog;

use Illuminate\Contracts\Foundation\Application;
use Illuminate\Http\Client\Factory;
use Illuminate\Support\ServiceProvider;

class CatalogServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->mergeConfigFrom(__DIR__.'/../config/catalog.php', 'acme-catalog');

        $this->app->bind(CatalogClient::class, function (Application $app): CatalogClient {
            return new CatalogClient(
                $app->make(Factory::class),
                $app['config']->get('acme-catalog.base_url'),
            );
        });
    }

    public function boot(): void
    {
        $this->publishes([
            __DIR__.'/../config/catalog.php' => config_path('acme-catalog.php'),
        ], 'acme-catalog-config');
    }
}
```

El provider se carga, por ejemplo, mediante el [descubrimiento automático de paquetes](https://kawax.biz/es/advanced/package-discovery.md). Quien use el paquete publica la configuración y cambia la URL base en `config/acme-catalog.php` por la de la API real. El `catalog.example.test` del ejemplo es un host ficticio para la explicación y los fakes.

```shell theme={null}
php artisan vendor:publish --tag=acme-catalog-config
```

En la aplicación, el cliente se resuelve mediante inyección en el constructor o desde el contenedor. La solicitud para obtener el producto se envía por primera vez en esta llamada.

```php theme={null}
use Acme\Catalog\CatalogClient;

$product = app(CatalogClient::class)->product(42, 'ja');
```

El registro usa `bind()`, pero si el objeto que guarda el cliente es un singleton, esa referencia no se vuelve a crear cada vez. Lo importante es no guardar en propiedades el idioma, las credenciales ni los objetos de solicitud propios de cada llamada. Para más detalles, consulta [Diseño del ciclo de vida de los servicios de paquetes](https://kawax.biz/es/advanced/package-service-lifetimes.md).

La sobrescritura de la configuración y la actualización de la caché durante el despliegue se tratan en [Fusión y caché de la configuración de paquetes](https://kawax.biz/es/advanced/package-config-merging.md). Gestiona la URL base como configuración de confianza de la aplicación y no permitas que la entrada de una solicitud determine libremente el destino de la conexión.

## Verificar el cliente real con fakes

Si haces un mock del propio cliente, no podrás comprobar si usa la Factory compartida ni si construye correctamente la URL y las cabeceras. En las pruebas del paquete, **resuelve el `CatalogClient` real desde el contenedor y simula solo las respuestas HTTP**.

El siguiente ejemplo se ejecuta en el [entorno de pruebas de Orchestra Testbench](https://kawax.biz/es/advanced/package-testing.md). En el `setUp()` de cada prueba se activa `preventStrayRequests()`. En las pruebas que realizan comunicaciones, configura respuestas solo para las URL esperadas y no añadas un comodín `'*'`; así también detectarás errores en la URL.

```php theme={null}
<?php

namespace Acme\Catalog\Tests;

use Acme\Catalog\CatalogClient;
use Acme\Catalog\CatalogServiceProvider;
use Illuminate\Http\Client\Request;
use Illuminate\Http\Client\RequestException;
use Illuminate\Http\Client\StrayRequestException;
use Illuminate\Support\Facades\Http;
use Orchestra\Testbench\TestCase;

class CatalogClientTest extends TestCase
{
    protected function getPackageProviders($app): array
    {
        return [CatalogServiceProvider::class];
    }

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

        Http::preventStrayRequests();
    }

    public function test_resolving_the_client_does_not_send_a_request(): void
    {
        Http::fake();

        $this->assertInstanceOf(CatalogClient::class, $this->app->make(CatalogClient::class));

        Http::assertNothingSent();
    }

    public function test_product_request_uses_the_shared_factory(): void
    {
        $client = $this->app->make(CatalogClient::class);

        Http::fake([
            'https://catalog.example.test/api/products/42' => Http::response([
                'id' => 42,
                'name' => 'Notebook',
            ]),
        ]);

        $this->assertSame(['id' => 42, 'name' => 'Notebook'], $client->product(42, 'ja'));

        Http::assertSent(static fn (Request $request): bool =>
            $request->method() === 'GET'
            && $request->url() === 'https://catalog.example.test/api/products/42'
            && $request->hasHeader('Accept', 'application/json')
            && $request->hasHeader('Accept-Language', 'ja')
        );
        Http::assertSentCount(1);
    }

    public function test_language_is_not_carried_over_to_the_next_request(): void
    {
        Http::fake([
            'https://catalog.example.test/api/products/42' => Http::response(['id' => 42]),
        ]);

        $client = $this->app->make(CatalogClient::class);
        $client->product(42, 'ja');
        $client->product(42);

        Http::assertSentInOrder([
            static fn (Request $request): bool => $request->hasHeader('Accept-Language', 'ja'),
            static fn (Request $request): bool => ! $request->hasHeader('Accept-Language'),
        ]);
    }

    public function test_server_errors_are_not_returned_as_product_data(): void
    {
        Http::fake([
            'https://catalog.example.test/api/products/42' => Http::response(['error' => 'unavailable'], 503),
        ]);

        $this->expectException(RequestException::class);

        $this->app->make(CatalogClient::class)->product(42);
    }

    public function test_a_request_without_a_matching_fake_is_rejected(): void
    {
        Http::fake([]);

        $this->expectException(StrayRequestException::class);

        $this->app->make(CatalogClient::class)->product(42);
    }
}
```

En la segunda prueba, **el fake se configura después de resolver el cliente**. Como el cliente guarda la Factory y crea el PendingRequest en el momento de la llamada real, el fake se aplica incluso en este orden. Si cambias el diseño para crear y guardar el PendingRequest en el constructor, la prueba detectará que los fakes añadidos después de crearlo no se aplican.

`assertSent()` verifica que exista al menos una solicitud coincidente. Para no pasar por alto envíos adicionales no previstos, combínalo con `assertSentCount()`. `assertSentInOrder()` verifica el número y el orden de las solicitudes, por lo que también sirve para comprobar el aislamiento del estado al usar el mismo cliente varias veces seguidas.

La protección se configura después de `parent::setUp()`. Esta prueba por sí sola no permite verificar las comunicaciones realizadas mientras arranca el provider, así que mantén un diseño en el que el registro y el arranque no realicen comunicaciones.

> [!NOTE]
>
> Lo que se puede comprobar con fakes es la solicitud que construye el cliente y el procesamiento de las respuestas preparadas. No garantiza la autenticación del servicio real, la conexión TLS, la duración real de los tiempos de espera ni los cambios en la especificación de la API externa. Las verificaciones que implican conexiones externas se realizan por separado en un entorno de verificación preparado de forma explícita.

## Verificaciones adicionales para el mantenimiento a largo plazo

| Cambio o caso de error | Qué comprobar |
| - | - |
| Sobrescritura de la URL base | Se aplica la configuración de la aplicación y la solicitud se envía a la ruta prevista |
| Fallo de conexión | Con `Http::failedConnection()`, la `ConnectionException` llega a la aplicación |
| JSON no válido o tipo inesperado | El JSON no válido produce `JsonException`, y un valor que no es un array produce la `UnexpectedValueException` del ejemplo |
| Añadir reintentos | Verifica el número de intentos, los códigos de estado afectados y el contenido enviado, y ten en cuenta las ejecuciones duplicadas en las API con efectos secundarios |
| Añadir autenticación o idioma | Aunque se use el mismo cliente varias veces, los valores de la llamada anterior no se arrastran |
| Actualización de Laravel | En las versiones compatibles, sigue comprobando la ruta de resolución de la Factory, la aplicación de los fakes y el contrato de excepciones |

Si separas las responsabilidades de la Factory compartida y de cada PendingRequest, la aplicación puede usar directamente los métodos de prueba habituales de Laravel. En lugar de crear primero un mecanismo de fakes propio, prepara una implementación que pase por la Factory compartida y pruebas que lo comprueben.

## Fuentes primarias consultadas

* [Documentación oficial de Laravel: cliente HTTP](https://github.com/laravel/docs/blob/13.x/http-client.md)
* [Documentación oficial de Laravel: desarrollo de paquetes](https://github.com/laravel/docs/blob/13.x/packages.md)
* [Framework v13.39.0: fake, createPendingRequest y \_\_call de Factory](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Http/Client/Factory.php)
* [Framework v13.39.0: configuración de PendingRequest y procesamiento de stubs](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Http/Client/PendingRequest.php)
* [Framework v13.39.0: throw y decodificación JSON de Response](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Http/Client/Response.php)
* [Framework v13.39.0: destino de resolución y fake de la fachada Http](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Support/Facades/Http.php)
* [Framework v13.39.0: registro singleton en FoundationServiceProvider](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php)
* [Tests oficiales de Laravel: HttpClientTest](https://github.com/laravel/framework/blob/v13.39.0/tests/Http/HttpClientTest.php)


## Related topics

- [Cliente HTTP](https://kawax.biz/es/http-client.md)
- [Temas avanzados](https://kawax.biz/es/advanced/index.md)
- [Guía de desarrollo de apps con integración de la Engine API - VOICEVOX for Laravel](https://kawax.biz/es/packages/laravel-voicevox/app-guide.md)
- [Pruebas HTTP](https://kawax.biz/es/http-tests.md)
- [Modo cliente - Presets - VOICEVOX for Laravel](https://kawax.biz/es/packages/laravel-voicevox/client-presets.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.