Module: Posthaste::ErrorMapping
- Defined in:
- lib/posthaste/errors.rb
Overview
Mapping a refusal onto a class.
Constant Summary collapse
- BY_TYPE =
By TYPE first, because the type is the stable contract and the status is not specific enough. Only types that say more than their status does are listed; everything else falls through to BY_STATUS, which is the honest answer for a refusal reason this version has never seen.
{ 'unauthorized' => AuthenticationError, 'unauthenticated' => AuthenticationError, 'csrf_failed' => AuthenticationError, 'email_unverified' => AuthenticationError, 'forbidden' => PermissionDeniedError, 'invalid_request' => InvalidRequestError, 'not_found' => NotFoundError, 'conflict' => ConflictError, 'address_taken' => ConflictError, 'suppressed' => SuppressedError, 'suppression_protected' => UnprocessableError, 'suppression_platform' => UnprocessableError, 'suppression_hard_bounce' => UnprocessableError, 'domain_not_verified' => DomainNotVerifiedError, 'content_blocked' => ContentBlockedError, 'attachments_too_many' => AttachmentError, 'attachments_too_large' => AttachmentError, 'attachment_type_blocked' => AttachmentError, 'attachment_invalid' => AttachmentError, 'schedule_too_far' => ScheduleError, 'not_scheduled' => ScheduleError, 'unknown_template' => TemplateError, 'invalid_template' => TemplateError, 'template_in_use' => TemplateError, 'unknown_stream' => StreamError, # The four 429s. Two transient, two not. 'rate_limited' => RateLimited, 'platform_paused' => RateLimited, 'daily_limit_reached' => QuotaExhausted, 'monthly_limit_reached' => QuotaExhausted, 'internal' => ServerError # `connection_error` and `timeout` are deliberately ABSENT. They are # synthesised by the transport, which constructs the exception directly, # so an entry here would only ever fire on a response that carried one of # those strings as an HTTP-level refusal — and mapping a real 408 onto # ConnectionError ("never reached the server") would be a lie about what # happened. }.freeze
- BY_STATUS =
{ 400 => InvalidRequestError, 401 => AuthenticationError, 403 => PermissionDeniedError, 404 => NotFoundError, 409 => ConflictError, 422 => UnprocessableError, # An unrecognised 429 is treated as transient rather than as spent quota. # That direction is the safe one: a wrongly-retried throttle costs a few # seconds, while a wrongly-abandoned one drops mail that would have gone. 429 => RateLimited }.freeze
Class Method Summary collapse
- .class_for(error_type, status) ⇒ Object
-
.from_response(status, raw_body, retry_after_header, request_id: nil, api_key: nil) ⇒ Object
Build the exception for one refused response.
-
.parse_retry_after(header, envelope = {}) ⇒ Object
Retry-Afteris seconds or an HTTP-date.
Class Method Details
.class_for(error_type, status) ⇒ Object
259 260 261 262 263 264 265 |
# File 'lib/posthaste/errors.rb', line 259 def class_for(error_type, status) known = BY_TYPE[error_type] return known if known return ServerError if status >= 500 BY_STATUS.fetch(status, APIStatusError) end |
.from_response(status, raw_body, retry_after_header, request_id: nil, api_key: nil) ⇒ Object
Build the exception for one refused response.
268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 |
# File 'lib/posthaste/errors.rb', line 268 def from_response(status, raw_body, retry_after_header, request_id: nil, api_key: nil) body = begin parsed = raw_body.to_s.empty? ? nil : JSON.parse(raw_body) parsed.is_a?(Hash) ? parsed : nil rescue JSON::ParserError nil end envelope = body.is_a?(Hash) && body['error'].is_a?(Hash) ? body['error'] : {} type = envelope['type'].is_a?(String) ? envelope['type'] : 'unknown_error' = envelope['message'].is_a?(String) ? envelope['message'] : "HTTP #{status}" class_for(type, status).new( , type: type, status: status, body: body, request_id: request_id, retry_after_seconds: parse_retry_after(retry_after_header, envelope), api_key: api_key ) end |
.parse_retry_after(header, envelope = {}) ⇒ Object
Retry-After is seconds or an HTTP-date. The body may also carry it as
retryAfterSeconds, and the body wins when both are present because it
is the value the limiter actually computed.
294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 |
# File 'lib/posthaste/errors.rb', line 294 def parse_retry_after(header, envelope = {}) from_body = envelope['retryAfterSeconds'] return from_body.to_f if from_body.is_a?(Numeric) return nil if header.nil? || header.to_s.strip.empty? text = header.to_s.strip return text.to_f if /\A\d+(\.\d+)?\z/.match?(text) begin seconds = Time.httpdate(text) - Time.now seconds.positive? ? seconds : 0.0 rescue ArgumentError nil end end |