Exception: Nombaone::APIError

Inherits:
Error
  • Object
show all
Defined in:
lib/nombaone/errors.rb,
lib/nombaone/errors.rb,
sig/nombaone/errors.rbs

Overview

A non-2xx response from the API. Carries everything the error envelope said: the stable #code to branch on, the #hint telling you how to fix it, the #doc_url into the error reference, per-field validation errors on 422s, and the #request_id to quote to support.

Subclasses are keyed by HTTP status so rescue reads naturally: AuthenticationError, RateLimitError, ValidationError, …. Branch on #code (stable) or the class (by HTTP status), never on the message (it may be reworded).

Constant Summary collapse

CLASS_FOR_STATUS =

Subclass dispatch by HTTP status (429 is special-cased in from_response).

Returns:

{
  400 => BadRequestError,
  401 => AuthenticationError,
  403 => PermissionDeniedError,
  404 => NotFoundError,
  409 => ConflictError,
  422 => ValidationError,
}.freeze
DEFAULT_CODE_FOR_STATUS =

The code to assume when the error body is missing or unusable.

Returns:

  • (Hash[Integer, String])
{
  400 => ErrorCode::CLIENT_INVALID_REQUEST,
  401 => ErrorCode::API_KEY_INVALID,
  403 => ErrorCode::CLIENT_FORBIDDEN,
  404 => ErrorCode::CLIENT_RESOURCE_NOT_FOUND,
  409 => ErrorCode::CLIENT_CONFLICT,
  422 => ErrorCode::CLIENT_VALIDATION_FAILED,
  429 => ErrorCode::RATE_LIMIT_EXCEEDED,
  502 => ErrorCode::SYSTEM_UPSTREAM_ERROR,
  503 => ErrorCode::SYSTEM_UPSTREAM_ERROR,
  504 => ErrorCode::SYSTEM_UPSTREAM_ERROR,
}.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(message, status_code:, code:, hint: "", doc_url: "", fields: nil, request_id: nil) ⇒ APIError

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns a new instance of APIError.



130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/nombaone/errors.rb', line 130

def initialize(message, status_code:, code:, hint: "", doc_url: "", fields: nil,
               request_id: nil)
  # Surface the hint in the raised message itself — the fix should arrive
  # with the failure, without a docs tab.
  super(hint.nil? || hint.empty? ? message : "#{message}#{hint}")
  @status_code = status_code
  @code = code
  @hint = hint || ""
  @doc_url = doc_url || ""
  @fields = fields
  @request_id = request_id
end

Instance Attribute Details

#codeString (readonly)

Returns stable machine-readable error code — branch on this.

Returns:

  • (String)

    stable machine-readable error code — branch on this.



118
119
120
# File 'lib/nombaone/errors.rb', line 118

def code
  @code
end

#doc_urlString (readonly)

Returns deep link to this code's entry in the error reference.

Returns:

  • (String)

    deep link to this code's entry in the error reference.



122
123
124
# File 'lib/nombaone/errors.rb', line 122

def doc_url
  @doc_url
end

#fieldsHash{String => Array<String>}? (readonly)

Returns per-field validation errors, present on 422 responses.

Returns:

  • (Hash{String => Array<String>}, nil)

    per-field validation errors, present on 422 responses.



125
126
127
# File 'lib/nombaone/errors.rb', line 125

def fields
  @fields
end

#hintString (readonly)

Returns actionable guidance from the API on what to do next.

Returns:

  • (String)

    actionable guidance from the API on what to do next.



120
121
122
# File 'lib/nombaone/errors.rb', line 120

def hint
  @hint
end

#request_idString? (readonly)

Returns the request id — include it when contacting support.

Returns:

  • (String, nil)

    the request id — include it when contacting support.



127
128
129
# File 'lib/nombaone/errors.rb', line 127

def request_id
  @request_id
end

#status_codeInteger (readonly)

Returns HTTP status of the response.

Returns:

  • (Integer)

    HTTP status of the response.



116
117
118
# File 'lib/nombaone/errors.rb', line 116

def status_code
  @status_code
end

Class Method Details

.default_code_for_status(status) ⇒ String

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Parameters:

  • status (Integer)

Returns:

  • (String)


177
178
179
# File 'lib/nombaone/errors.rb', line 177

def self.default_code_for_status(status)
  DEFAULT_CODE_FOR_STATUS.fetch(status, ErrorCode::SYSTEM_INTERNAL_ERROR)
end

.from_response(status, body, headers) ⇒ APIError

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Build the right Nombaone::APIError subclass from a raw response.

Parameters:

  • status (Integer)
  • body (Object, nil)

    the parsed JSON body (nil if it was not JSON).

  • headers (#[])

    response headers, looked up case-insensitively by lowercase name.

Returns:



151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/nombaone/errors.rb', line 151

def self.from_response(status, body, headers)
  parsed = parse_error_body(body)
  code = parsed[:code] || default_code_for_status(status)
  message = parsed[:message] || "Request failed with status #{status}"
  details = {
    status_code: status,
    code: code,
    hint: parsed[:hint] || "",
    doc_url: parsed[:doc_url] || "",
    request_id: parsed[:request_id] || headers["x-request-id"],
    fields: parsed[:fields],
  }

  klass = CLASS_FOR_STATUS[status] || (status >= 500 ? ServerError : APIError)
  return klass.new(message, **details) unless status == 429

  RateLimitError.new(
    message,
    retry_after: numeric(headers["retry-after"]),
    limit: numeric(headers["x-ratelimit-limit"]),
    remaining: numeric(headers["x-ratelimit-remaining"]),
    **details,
  )
end