Ir al contenido principal
Laravel, shipping fast.

La License API depende de Statamic. Cuando Statamic no contesta, el cliente HTTP lanza una ConnectionException, y por defecto el consumidor ve un 500.

Esa es la respuesta equivocada. Un 500 dice «esta API tiene un bug». La verdad es «no se puede alcanzar al proveedor que está detrás de esta API», que es un 504, y un consumidor puede reintentarlo razonablemente.

// bootstrap/app.php
$exceptions->render(function (ConnectionException $e) {
    return response()->json([
        'message' => __('errors.provider_unavailable'),
        'code' => ErrorCode::ProviderUnavailable,
    ], Response::HTTP_GATEWAY_TIMEOUT);
});

ErrorCode es un enum que este capítulo presenta en un momento.

La declaración de tipo del closure es el enrutamiento. Laravel lo llama solo para esa excepción y recurre a sus valores por defecto para todo lo demás, así que no hay un match sobre clases de excepción que mantener. (Documentación de Laravel: Error Handling › Rendering Exceptions.)

La respuesta no contiene $e->getMessage(). El mensaje de la excepción lleva dentro el nombre de host del proveedor. Renderices lo que renderices a partir de un fallo de un servicio externo, escribe la frase tú mismo.

Lo mismo vale para un proveedor que responde con un error. El throw() del driver lanza una RequestException, y eso se convierte en un 502. Pero no todos son iguales. Un 5xx del proveedor es un problema del proveedor y puede arreglarse solo. Un 4xx significa que el proveedor entendió la petición y la rechazó, y enviarla otra vez no le hará cambiar de opinión. La única excepción es un 429 que sobrevivió a los reintentos del driver, que va con los 5xx:

// bootstrap/app.php
$exceptions->render(function (RequestException $e) {
    $temporary = $e->response->serverError()
        || $e->response->tooManyRequests();

    $code = $temporary
        ? ErrorCode::ProviderFailed
        : ErrorCode::ProviderRejected;

    return response()->json([
        'message' => __("errors.{$code->value}"),
        'code' => $code,
    ], Response::HTTP_BAD_GATEWAY);
});

El MalformedProviderResponse del Capítulo 3 es el tercer miembro de esta familia, y la sección siguiente lo muestra.

Códigos de error sobre los que un consumidor puede decidir

Un código de estado le dice a un consumidor qué clase de cosa salió mal. No le dice qué salió mal en concreto. Un 504 porque el proveedor no respondió a tiempo y un 504 de un proxy que tienes delante son idénticos.

Un mensaje no sirve para decidir, porque el Capítulo 6 hizo que esta API responda en el idioma del consumidor. Un cliente que hace if (error.message === 'Provider unavailable') se rompe en el momento en que ese mensaje vuelve en español.

Así que los fallos de dominio llevan un código estable e independiente del idioma:

// app/Enums/ErrorCode.php
enum ErrorCode: string
{
    case ProviderUnavailable = 'provider_unavailable';
    case ProviderFailed = 'provider_failed';
    case ProviderRejected = 'provider_rejected';
    case LicenseLimitReached = 'license_limit_reached';
    case RequestInProgress = 'request_in_progress';
}
{
    "message": "The license provider is not responding.",
    "code": "provider_unavailable"
}

Un consumidor decide según code, que nunca cambia sea cual sea el idioma en que se renderice el mensaje. Los dos primeros merecen un reintento. El tercero no. Los dos últimos son ambos conflictos, y solo el código le dice al consumidor que uno de ellos se resolverá solo en un segundo y el otro nunca. El estado dice qué clase de cosa pasó. El código dice qué hacer al respecto.

Mantén la lista corta. Un código es una promesa: una vez que un consumidor decide según él, no puedes renombrarlo. Añade uno cuando un consumidor tenga que distinguir dos fallos, no antes.

Cuando una excepción pertenece a tu propio dominio, puede renderizarse a sí misma. La excepción del Capítulo 3 para un proveedor que envió algo sin sentido lo hace:

// app/Exceptions/MalformedProviderResponse.php
class MalformedProviderResponse extends RuntimeException
{
    /** @param  array<int, string>  $fields */
    public function __construct(public array $fields)
    {
        parent::__construct(
            'Malformed provider response: '
            .implode(', ', $fields),
        );
    }

    public function render(): JsonResponse
    {
        return response()->json([
            'message' => __('errors.provider_failed'),
            'code' => ErrorCode::ProviderFailed,
        ], Response::HTTP_BAD_GATEWAY);
    }
}

El mensaje es para tu log, y contiene nombres de campos, nunca valores. La respuesta es el 502 que recibe un proveedor que falla, porque eso es lo que suele ser una respuesta mal formada: una página de error donde debería haber estado el JSON.

Lánzala desde cualquier sitio y la respuesta es la misma. (Documentación de Laravel: Error Handling › Reportable and Renderable Exceptions.)

LicenseLimitReached tiene la misma forma con otra respuesta:

// app/Exceptions/LicenseLimitReached.php
public function render(): JsonResponse
{
    return response()->json([
        'message' => __('errors.license_limit_reached'),
        'code' => ErrorCode::LicenseLimitReached,
    ], Response::HTTP_CONFLICT);
}

Se lanza en store, antes de pedirle al proveedor que cree nada. Cada consumidor tiene un license_limit, y a un consumidor que está en su límite se le rechaza:

// app/Http/Controllers/LicenseController.php, en store()
$limit = $request->user()->license_limit;

if (Licenses::all()->count() >= $limit) {
    throw new LicenseLimitReached;
}

El Capítulo 2 trazó la línea entre validación y regla de negocio preguntando si el consumidor podía arreglarlo enviando algo distinto. Aquí no puede. Ningún campo está mal. Así que esto es una excepción con un código, y no un 422.

No se pudo cargar el audio. Inténtalo de nuevo en un momento.