Ошибки
Каждый сбой выбрасывает исключение. Один класс для отказа, один для отсутствия ответа и идентификатор запроса в каждой ошибке API.
Как его поймать
message = {from: "[email protected]", to: "[email protected]", subject: "Your September invoice", text: "Invoice attached."} begin client.emails.send(message)rescue OpenEmail::ApiError => error warn "#{error.code} #{error.param} #{error.message}" if error.validation? warn client.addresses.list_all.addresses.inspect if error.permission? warn "try again in #{error.retry_after_seconds} seconds" if error.rate_limited? warn "#{error.status} #{error.request_id}" raiserescue OpenEmail::NetworkError => error warn "no answer in time" if error.timeout? raiseendpermission_error при отправке обычно связан с областью отправки самого КЛЮЧА, то есть с доменом или адресом, которые ему не выданы, а не с рабочим пространством. Поэтому пример печатает то, что addresses.list_all сообщает о том, от чьего имени этот ключ может отправлять.
У каждого вида отказа есть собственный подкласс, поэтому rescue может выбрать по классу те, которые обрабатывает, а остальные пропустить выше.
message = {from: "[email protected]", to: "[email protected]", subject: "Your September invoice", text: "Invoice attached."} begin client.emails.send(message)rescue OpenEmail::ValidationError => error warn "#{error.param}: #{error.message}"rescue OpenEmail::AuthenticationError, OpenEmail::PermissionError => error warn "the key cannot do this: #{error.code}" raiserescue OpenEmail::Error => error warn "#{error.class}: #{error.message}" raiseendКлассы
| Класс | Когда |
|---|---|
| OpenEmail::Error | Базовый класс всех ошибок, которые определяет гем, поэтому rescue OpenEmail::Error перехватывает их все. ArgumentError он не перехватывает. |
| OpenEmail::ApiError | API ответил, и ответ не был успешным. Несёт status, type, code, param, doc_url, request_id, retry_after_seconds, fields и body. Выбрасывается как есть, когда type равен api_error, как при сбое сервера, а в остальных случаях как подкласс для своего type. |
| OpenEmail::InvalidRequestError, AuthenticationError, PermissionError, NotFoundError, ConflictError, ValidationError и RateLimitError | Подклассы ApiError, по одному на каждый type: invalid_request_error, authentication_error, permission_error, not_found_error, conflict_error, validation_error и rate_limit_error. |
| OpenEmail::NetworkError | Ответ не пришёл: DNS, TLS, отклонённое или оборванное соединение либо таймаут. Несёт original, исходное исключение, которое также является его cause, а timeout? равно true, когда причиной был таймаут. |
| OpenEmail::WebhookSignatureError | OpenEmail.verify_webhook_signature отклонил доставку. |
| ArgumentError | Выбрасывается до того, как что-либо отправлено: отсутствующий или неправильно сформированный ключ, непригодный base_url:, пустой идентификатор. Это обычный класс Ruby, а не OpenEmail::Error, потому что он означает, что неверен сам вызов. |
Что несёт ApiError
messageString- Собственная фраза API, написанная для человека и называющая проблемное значение, если оно есть. Это не стабильный идентификатор, поэтому ветвитесь по `code`.
statusInteger- HTTP-статус ответа.
typeString- Одно из восьми значений в `OpenEmail::ERROR_TYPES`, замороженном наборе, который не будет расти. Если тело его не называет, оно выводится из статуса.
codeString- Конкретный сбой, например `from_address_forbidden` или `invalid_email_address`. Набор открытый и пополняемый, поэтому незнакомый код обрабатывайте по его `type`. Равен `unrecognised_response`, когда тело не было конвертом ошибки API.
paramString or nil- Отклонённое поле в виде пути через точку, например `to.0`, если сбой его называет.
doc_urlString or nil- Страница об этом сбое, если API её называет.
request_idString or nil- Идентификатор, под которым сервер записал запрос в журнал, из тела или заголовка `x-request-id`.
retry_after_secondsInteger, Float or nil- Время ожидания в секундах, которое сервер запросил в `Retry-After`, прислал ли он число или дату. nil, если он ничего не прислал.
fieldsArray<Hash> or nil- По одному Hash на каждую проблему, в каждом `key` и `error`, например `{key: "email", error: "email"}`, когда `forms.subscribe` отклонил ответы с 422 `invalid_form_submission`. nil, если ошибка не перечисляет проблем.
bodyHash or nil- Весь ответ с ошибкой, разобранный, с ключами типа Symbol. nil, если он был пуст или не был JSON.
| Предикат | True, когда |
|---|---|
| auth? | type равен authentication_error, 401: ключа нет, учётные данные не того типа или ключ, который выдавали не мы. |
| permission? | permission_error, 403: настоящий ключ без нужной области или без нужного адреса From. |
| scope_missing? | code равен insufficient_scope: это тот самый 403, который называет недостающую область. |
| invalid_request? | invalid_request_error, 400: запрос, который не удалось понять. Сообщение сверх предельного размера возвращается как 422 message_too_large, поэтому его ловит предикат validation?. |
| validation? | validation_error, 422: схема отклонила запрос, а param называет поле. |
| not_found? | not_found_error, 404: такого ресурса нет. |
| conflict? | conflict_error, 409: ресурс уже прошёл ту точку, в которой с ним можно было это сделать. |
| rate_limited? | rate_limit_error, 429. retry_after_seconds содержит время ожидания, если сервер его назвал. |
| server_error? | status равен 500 или выше. Укажите request_id, если обращаетесь в поддержку. |
| retryable? | status равен 408, 429, 500, 502, 503 или 504. |
| step_up_required? | code равен step_up_required: это 403, который токен доступа OAuth получает перед чувствительным изменением, пока человек не подтвердит код. |
Большинство предикатов читают type, замороженную половину конверта, и каждый подкласс соответствует одному type. code остаётся String, потому что API гарантирует, что этот набор открытый и пополняемый, поэтому незнакомый код обрабатывайте по его type. С закрытым списком ценой чтения нового вида сбоя стало бы обновление гема.
Тело, которое не является конвертом ошибки API, всё равно выбрасывает OpenEmail::ApiError с type, выведенным из статуса, и code, равным unrecognised_response. Успешный ответ, тело которого не является JSON, тоже его выбрасывает.
retryable? описывает статус, а не ваш вызов. Вызов, который безопасно повторять, к моменту исключения уже был повторён, а 429, например send_quota_exceeded или ai_quota_exceeded, будет завершаться так же, пока его лимит не обнулится, поэтому покажите его человеку, а не повторяйте в цикле.
Исключение, которое выбрасывает ваш адаптер, становится OpenEmail::NetworkError, когда исчерпаны все повторы, допустимые для вызова, а исходное исключение лежит в original и cause. Исключения из этого правила: NameError, TypeError и ArgumentError. Они означают ошибку в адаптере, поэтому выбрасываются без изменений и никогда не повторяются.
request_id
Каждый OpenEmail::ApiError несёт идентификатор запроса, который прислал сервер, из тела ошибки или заголовка x-request-id, и это единственное, что связывает ваш сбой со строкой в журнале сервера. Успешный ответ возвращает только разобранное тело, поэтому идентификатора запроса у него нет.