Errors
Every failure raises. One class for a refusal, one for no answer, and a request id on every API error.
Catching one
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? raiseendA permission_error on a send is usually the KEY’s send scope, a domain or an address it was not given, rather than the workspace, which is why the sample prints what addresses.list_all says this key may send as.
Each kind of refusal has a subclass of its own, so a rescue can pick the ones it handles by class and let the rest go on up.
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}" raiseendThe classes
| Class | When |
|---|---|
| OpenEmail::Error | The base of every error the gem defines, so rescue OpenEmail::Error catches all of them. It does not catch ArgumentError. |
| OpenEmail::ApiError | The API answered, and not with a success. Carries status, type, code, param, doc_url, request_id, retry_after_seconds, fields and body. Raised as itself when type is api_error, as a server fault is, and as the subclass for its type otherwise. |
| OpenEmail::InvalidRequestError, AuthenticationError, PermissionError, NotFoundError, ConflictError, ValidationError and RateLimitError | Subclasses of ApiError, one for each type: invalid_request_error, authentication_error, permission_error, not_found_error, conflict_error, validation_error and rate_limit_error. |
| OpenEmail::NetworkError | No response arrived: DNS, TLS, a refused or dropped connection, or the timeout. Carries original, the exception underneath, which is also its cause, and timeout? is true when the timeout was the reason. |
| OpenEmail::WebhookSignatureError | OpenEmail.verify_webhook_signature refused a delivery. |
| ArgumentError | Raised before anything is sent: a missing or malformed key, an unusable base_url:, an empty id. It is the plain Ruby class, not an OpenEmail::Error, because it means the call itself is wrong. |
What an ApiError carries
messageString- The API’s own sentence, written for a person and naming the offending value where there is one. Not a stable identifier, so switch on `code`.
statusInteger- The HTTP status of the answer.
typeString- One of the eight values in `OpenEmail::ERROR_TYPES`, a set that is frozen and will not grow. When the body names none, it is inferred from the status.
codeString- The specific failure, such as `from_address_forbidden` or `invalid_email_address`. Open and additive, so treat one you do not recognise as its `type`. It is `unrecognised_response` when the body was not the API’s error envelope.
paramString or nil- The field that was refused, as a dotted path such as `to.0`, when the failure names one.
doc_urlString or nil- A page about this failure, when the API names one.
request_idString or nil- The id the server logged the request under, from the body or the `x-request-id` header.
retry_after_secondsInteger, Float or nil- The wait the server asked for in `Retry-After`, in seconds, whether it sent a number or a date. nil when it sent none.
fieldsArray<Hash> or nil- One Hash per problem, each with `key` and `error`, such as `{key: "email", error: "email"}` when `forms.subscribe` refused the answers with a 422 `invalid_form_submission`. nil when the error lists none.
bodyHash or nil- The whole error response, parsed, with Symbol keys. nil when it was empty or not JSON.
| Predicate | True when |
|---|---|
| auth? | type is authentication_error, a 401: no key, the wrong kind of credential, or a key we did not issue. |
| permission? | permission_error, a 403: a real key without the scope or the From address it needs. |
| scope_missing? | code is insufficient_scope, the 403 that names a missing scope. |
| invalid_request? | invalid_request_error, a 400: a request that could not be understood. A message over the size ceiling comes back as a 422 message_too_large, so validation? is the predicate that catches it. |
| validation? | validation_error, a 422: the schema refused it, and param names the field. |
| not_found? | not_found_error, a 404: no such resource. |
| conflict? | conflict_error, a 409: the resource is past the point where this could be done to it. |
| rate_limited? | rate_limit_error, a 429. retry_after_seconds holds the wait when the server named one. |
| server_error? | status is 500 or above. Quote request_id if you contact support. |
| retryable? | status is 408, 429, 500, 502, 503 or 504. |
| step_up_required? | code is step_up_required, the 403 an OAuth access token gets before a sensitive change until the person has verified a code. |
Most predicates read type, the frozen half of the envelope, and each subclass stands for one type. code stays a String, because the API guarantees it is open and additive, so treat one you do not recognise as its type. A closed list would make a gem upgrade the price of reading a new failure mode.
A body that is not the API’s error envelope still raises an OpenEmail::ApiError, with type inferred from the status and code set to unrecognised_response. A success whose body is not JSON raises one too.
retryable? describes the status, not your call. A call that is safe to repeat has already been retried by the time it raises, and a 429 such as send_quota_exceeded or ai_quota_exceeded fails the same way until its allowance resets, so show it to a person rather than looping on it.
An exception your adapter raises becomes an OpenEmail::NetworkError once any retries the call allows are spent, with the original on original and cause. NameError, TypeError and ArgumentError are the exceptions: they mean a bug in the adapter, so they are raised unchanged and never retried.
request_id
Every OpenEmail::ApiError carries the request id the server sent, from the error body or the x-request-id header, and it is the only thing tying your failure to a line in the server’s log. A success returns the parsed body alone, so there is no request id to read on one.