Перейти к документации
PHP

Ошибки

Каждый сбой выбрасывает исключение. Один класс для отказа, один для отсутствия ответа и идентификатор запроса в каждой ошибке API.

Как его поймать

catch_errors.php
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;}

permission_error при отправке обычно связан с областью отправки самого КЛЮЧА, то есть с доменом или адресом, которые ему не выданы, а не с рабочим пространством. Поэтому пример записывает в журнал то, что addresses->listAll() сообщает о том, от чьего имени этот ключ может отправлять.

У каждого вида отказа есть собственный подкласс, поэтому catch может выбрать по классу те, которые обрабатывает, а остальные пропустить выше.

catch_by_class.php
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;}

Классы

Все классы находятся в OpenEmail\Exception.

КлассКогда
OpenEmailExceptionИнтерфейс, который реализует каждое исключение, выбрасываемое пакетом, поэтому catch (OpenEmailException $error) перехватывает их все, включая InvalidArgumentException.
ApiExceptionAPI ответил, и ответ не был успешным. Несёт status, type, errorCode, param, docUrl, requestId, retryAfterSeconds, fields и body. Выбрасывается как есть, когда type равен api_error, как при сбое сервера, а в остальных случаях как подкласс для своего type. Наследует RuntimeException.
InvalidRequestException, AuthenticationException, PermissionException, NotFoundException, ConflictException, ValidationException и RateLimitExceptionПодклассы ApiException, по одному на каждый type: invalid_request_error, authentication_error, permission_error, not_found_error, conflict_error, validation_error и rate_limit_error.
NetworkExceptionОтвет не пришёл: DNS, TLS, отклонённое или оборванное соединение либо таймаут. getPrevious() хранит исходное исключение, а isTimeout() равно true, когда причиной был таймаут. Наследует RuntimeException.
WebhookSignatureExceptionOpenEmail::verifyWebhookSignature() отклонил доставку. Наследует UnexpectedValueException.
InvalidArgumentExceptionВыбрасывается до того, как что-либо отправлено: отсутствующий или неправильно сформированный ключ, непригодный baseUrl:, пустой идентификатор. Наследует собственный InvalidArgumentException из PHP, потому что означает, что неверен сам вызов.

Что несёт ApiException

getMessage()string
Собственная фраза API, написанная для человека и называющая проблемное значение, если оно есть. Это не стабильный идентификатор, поэтому ветвитесь по `errorCode`.
statusint or null
HTTP-статус ответа, который также возвращает `getCode()`. Равен null, только когда успешный ответ пришёл в виде, который клиент не смог прочитать.
typestring
Одно из восьми значений в `OpenEmail\Constants\ErrorTypes`, фиксированном наборе, который не будет расти. Если тело его не называет, оно выводится из статуса.
errorCodestring
Конкретный сбой, например `from_address_forbidden` или `invalid_email_address`. Он называется `errorCode`, потому что в PHP `code` занят числом, которое возвращает `getCode()`. Набор открытый и пополняемый, поэтому незнакомый код обрабатывайте по его `type`. Равен `unrecognised_response`, когда тело не было конвертом ошибки API.
paramstring or null
Отклонённое поле в виде пути через точку, например `to.0`, если сбой его называет.
docUrlstring or null
Страница об этом сбое, если API её называет.
requestIdstring or null
Идентификатор, под которым сервер записал запрос в журнал, из тела или заголовка `x-request-id`.
retryAfterSecondsint, float or null
Время ожидания в секундах, которое сервер запросил в `Retry-After`, прислал ли он число или дату. null, если он ничего не прислал.
fieldsarray or null
По одному массиву на каждую проблему, в каждом `key` и `error`, например `['key' => 'email', 'error' => 'email']`, когда `forms->subscribe` отклонил ответы с 422 `invalid_form_submission`. null, если ошибка не перечисляет проблем.
bodymixed
Весь ответ с ошибкой, декодированный. null, если он был пуст или не был JSON.
МетодTrue, когда
isAuth()type равен authentication_error, 401: ключа нет, учётные данные не того типа или ключ, который выдавали не мы.
isPermission()permission_error, 403: настоящий ключ без нужной области или без нужного адреса From.
isScopeMissing()errorCode равен insufficient_scope: это 403, который называет недостающую область.
isInvalidRequest()invalid_request_error, 400: запрос, который не удалось разобрать. Письмо сверх предельного размера возвращается как 422 message_too_large, поэтому ловит его метод isValidation().
isValidation()validation_error, 422: схема отклонила запрос, а param называет поле.
isNotFound()not_found_error, 404: такого ресурса нет.
isConflict()conflict_error, 409: ресурс уже прошёл ту точку, в которой с ним можно было это сделать.
isRateLimited()rate_limit_error, 429. retryAfterSeconds содержит время ожидания, если сервер его назвал.
isServerError()status равен 500 или выше. При обращении в поддержку укажите requestId.
isRetryable()status равен 408, 429, 500, 502, 503 или 504.
isStepUpRequired()errorCode равен step_up_required: это 403, который токен доступа OAuth получает перед чувствительным изменением, пока человек не подтвердит код.

Большинство из них читают type, фиксированную половину конверта, и каждый подкласс соответствует одному type. errorCode остаётся строкой, потому что API гарантирует, что этот набор открытый и пополняемый, поэтому незнакомый код обрабатывайте по его type. С закрытым списком ценой чтения нового вида сбоя стало бы обновление пакета.

Тело, которое не является конвертом ошибки API, всё равно выбрасывает ApiException с type, выведенным из статуса, и errorCode, равным unrecognised_response. Успешный ответ, тело которого не является JSON, тоже его выбрасывает.

isRetryable() описывает статус, а не ваш вызов. Вызов, который безопасно повторять, к моменту исключения уже был повторён, а 429, например send_quota_exceeded или ai_quota_exceeded, будет завершаться так же, пока его лимит не обнулится, поэтому покажите его человеку, а не повторяйте в цикле.

Исключение, которое выбрасывает ваш HTTP-клиент, становится NetworkException, когда исчерпаны все повторы, допустимые для вызова, а исходное исключение доступно как getPrevious(). LogicException и любой Error, например TypeError, означают ошибку в самом клиенте, поэтому выбрасываются без изменений и никогда не повторяются. Psr18HttpClient сохраняет от исключения обёрнутого клиента только сообщение, потому что это исключение содержит запрос и его заголовок Authorization.

requestId

Каждый ApiException несёт идентификатор запроса, который прислал сервер, из тела ошибки или заголовка x-request-id, и это единственное, что связывает ваш сбой со строкой в журнале сервера. Успешный ответ возвращает только декодированное тело, поэтому идентификатора запроса у него нет.