Errores
Todo fallo lanza una excepción. Una clase para un rechazo, otra para la falta de respuesta, y un id de solicitud en cada error de la API.
Capturar uno
use OpenEmail\Exception\ApiException;use OpenEmail\Exception\NetworkException; $message = [ 'from' => '[email protected]', 'to' => '[email protected]', 'subject' => 'Your September invoice', 'text' => 'Invoice attached.',]; try { $client->emails->send($message);} catch (ApiException $error) { if ($error->isValidation()) { error_log($error->errorCode . ' ' . $error->param . ' ' . $error->getMessage()); } if ($error->isPermission()) { $book = $client->addresses->listAll(); error_log('this key may send as ' . implode(', ', array_column($book->addresses, 'address'))); } if ($error->isRateLimited()) { error_log('try again in ' . $error->retryAfterSeconds . ' seconds'); } error_log($error->status . ' ' . $error->requestId); throw $error;} catch (NetworkException $error) { if ($error->isTimeout()) { error_log('no answer in time'); } throw $error;}Un permission_error en un envío suele deberse al ámbito de envío de la CLAVE, a un dominio o a una dirección que no se le concedió, y no al espacio de trabajo, y por eso el ejemplo registra lo que dice addresses->listAll() sobre las direcciones con las que puede enviar esta clave.
Cada tipo de rechazo tiene su propia subclase, así que un catch puede elegir por clase los que gestiona y dejar que el resto siga subiendo.
use OpenEmail\Exception\AuthenticationException;use OpenEmail\Exception\OpenEmailException;use OpenEmail\Exception\PermissionException;use OpenEmail\Exception\ValidationException; $message = [ 'from' => '[email protected]', 'to' => '[email protected]', 'subject' => 'Your September invoice', 'text' => 'Invoice attached.',]; try { $client->emails->send($message);} catch (ValidationException $error) { error_log($error->param . ': ' . $error->getMessage());} catch (AuthenticationException|PermissionException $error) { error_log('the key cannot do this: ' . $error->errorCode); throw $error;} catch (OpenEmailException $error) { error_log($error::class . ': ' . $error->getMessage()); throw $error;}Las clases
Todas las clases están en OpenEmail\Exception.
| Clase | Cuándo |
|---|---|
| OpenEmailException | La interfaz que implementa toda excepción que lanza el paquete, así que catch (OpenEmailException $error) las captura todas, InvalidArgumentException incluida. |
| ApiException | La API respondió, y no con un éxito. Lleva status, type, errorCode, param, docUrl, requestId, retryAfterSeconds, fields y body. Se lanza tal cual cuando type es api_error, como ocurre con un fallo del servidor, y como la subclase de su type en los demás casos. Extiende RuntimeException. |
| InvalidRequestException, AuthenticationException, PermissionException, NotFoundException, ConflictException, ValidationException y RateLimitException | Subclases de ApiException, una por cada type: invalid_request_error, authentication_error, permission_error, not_found_error, conflict_error, validation_error y rate_limit_error. |
| NetworkException | No llegó ninguna respuesta: DNS, TLS, una conexión rechazada o cortada, o el tiempo de espera. getPrevious() contiene la excepción subyacente, e isTimeout() es true cuando el motivo fue el tiempo de espera. Extiende RuntimeException. |
| WebhookSignatureException | OpenEmail::verifyWebhookSignature() rechazó una entrega. Extiende UnexpectedValueException. |
| InvalidArgumentException | Se lanza antes de enviar nada: una clave ausente o mal formada, un baseUrl: inutilizable, un id vacío. Extiende la InvalidArgumentException propia de PHP, porque significa que la propia llamada es incorrecta. |
Lo que lleva una ApiException
getMessage()string- La frase de la propia API, escrita para una persona y que nombra el valor problemático cuando lo hay. No es un identificador estable, así que usa `errorCode` para ramificar.
statusint or null- El estado HTTP de la respuesta, que también devuelve `getCode()`. Es null solo cuando un éxito volvió con una forma que el cliente no pudo leer.
typestring- Uno de los ocho valores de `OpenEmail\Constants\ErrorTypes`, un conjunto fijo que no crecerá. Cuando el cuerpo no nombra ninguno, se infiere a partir del status.
errorCodestring- El fallo concreto, como `from_address_forbidden` o `invalid_email_address`. Se llama `errorCode` porque PHP reserva `code` para el número que devuelve `getCode()`. Es abierto y aditivo, así que trata uno que no reconozcas según su `type`. Vale `unrecognised_response` cuando el cuerpo no era el sobre de error de la API.
paramstring or null- El campo rechazado, como ruta con puntos, por ejemplo `to.0`, cuando el fallo nombra uno.
docUrlstring or null- Una página sobre este fallo, cuando la API indica una.
requestIdstring or null- El id con el que el servidor registró la solicitud, tomado del cuerpo o de la cabecera `x-request-id`.
retryAfterSecondsint, float or null- La espera que pidió el servidor en `Retry-After`, en segundos, tanto si envió un número como una fecha. null cuando no envió ninguna.
fieldsarray or null- Un array por problema, cada uno con `key` y `error`, como `['key' => 'email', 'error' => 'email']` cuando `forms->subscribe` rechazó las respuestas con un 422 `invalid_form_submission`. null cuando el error no enumera ninguno.
bodymixed- La respuesta de error completa, decodificada. null cuando estaba vacía o no era JSON.
| Método | True cuando |
|---|---|
| isAuth() | type es authentication_error, un 401: sin clave, un tipo de credencial equivocado o una clave que no emitimos nosotros. |
| isPermission() | permission_error, un 403: una clave real sin el scope o sin la dirección From que necesita. |
| isScopeMissing() | errorCode es insufficient_scope, el 403 que nombra un scope que falta. |
| isInvalidRequest() | invalid_request_error, un 400: una solicitud que no se pudo entender. Un mensaje que supera el límite de tamaño vuelve como un 422 message_too_large, así que isValidation() es el método que lo captura. |
| isValidation() | validation_error, un 422: el esquema lo rechazó, y param nombra el campo. |
| isNotFound() | not_found_error, un 404: no existe ese recurso. |
| isConflict() | conflict_error, un 409: el recurso ha pasado el punto en el que se le podía hacer esto. |
| isRateLimited() | rate_limit_error, un 429. retryAfterSeconds contiene la espera cuando el servidor indicó una. |
| isServerError() | status es 500 o superior. Cita requestId si contactas con soporte. |
| isRetryable() | status es 408, 429, 500, 502, 503 o 504. |
| isStepUpRequired() | errorCode es step_up_required, el 403 que recibe un token de acceso OAuth antes de un cambio delicado hasta que la persona haya verificado un código. |
La mayoría de estos leen type, la mitad fija del sobre, y cada subclase representa un type. errorCode sigue siendo una cadena, porque la API garantiza que es abierto y aditivo, así que trata uno que no reconozcas según su type. Una lista cerrada haría que actualizar el paquete fuera el precio de leer un nuevo tipo de fallo.
Un cuerpo que no es el sobre de error de la API lanza igualmente una ApiException, con type inferido a partir del status y errorCode fijado en unrecognised_response. Una respuesta correcta cuyo cuerpo no sea JSON también lanza una.
isRetryable() describe el status, no tu llamada. Una llamada que se puede repetir sin riesgo ya se ha reintentado cuando lanza la excepción, y un 429 como send_quota_exceeded o ai_quota_exceeded falla igual hasta que se restablece su cupo, así que muéstraselo a una persona en lugar de repetirlo en bucle.
Una excepción que lanza tu cliente HTTP se convierte en una NetworkException una vez agotados los reintentos que permita la llamada, con la original como getPrevious(). Una LogicException y cualquier Error, como un TypeError, indican un error de programación en él, así que se lanzan sin cambios y nunca se reintentan. Psr18HttpClient solo conserva el mensaje de una excepción del cliente que envuelve, porque esa excepción contiene la solicitud y su cabecera Authorization.
requestId
Toda ApiException lleva el id de solicitud que envió el servidor, tomado del cuerpo del error o de la cabecera x-request-id, y es lo único que vincula tu fallo con una línea del registro del servidor. Una respuesta correcta devuelve solo el cuerpo decodificado, así que en ella no hay ningún id de solicitud que leer.