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

Ошибки

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

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

rescue_errors.rb
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?  raiseend

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

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

rescue_by_class.rb
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::ApiErrorAPI ответил, и ответ не был успешным. Несёт 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::WebhookSignatureErrorOpenEmail.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, и это единственное, что связывает ваш сбой со строкой в журнале сервера. Успешный ответ возвращает только разобранное тело, поэтому идентификатора запроса у него нет.