Exception: X::HTTPError

Inherits:
Error
  • Object
show all
Includes:
RequestContext, ResponseHeaders
Defined in:
x-core/lib/x/core/errors/http_error.rb

Overview

Base class for HTTP errors from the X API

The message is what the API said went wrong, read from the body of the response, behind the method and path of the request it answered. #body holds that body as it arrived, #headers the headers it came with, and #problem the problem it describes the failure with as a whole, and #problems each problem it names, for code that acts on the reason rather than logging it. #http_method and #uri are the request the API refused.

A 4xx raises a ClientError, a 5xx a ServerError, and a body that is not JSON an InvalidResponse, each a subclass of this. It is raised itself for a redirect the client does not follow: a 300 Multiple Choices, 304 Not Modified, or 305 Use Proxy, and a redirect whose Location is missing, is not a valid URL, or is not an HTTP or HTTPS URL.

Direct Known Subclasses

ClientError, InvalidResponse, ServerError

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(message = nil, http_response: nil, status: nil, headers: nil, body: nil, http_method: nil, uri: nil) ⇒ HTTPError

Initialize a new HTTPError

Public, so that code that rescues an HTTPError, or a subclass such as NotFound, can be tested with one built from the status, headers, and body of a response, or from a Net::HTTP response, as x-core builds each from the response it parses. The error names the request, when given its method and URI, as x-core names the request the response answers.

It can be raised as any other exception is, as in raise X::NotFound, or raise X::NotFound, "gone", for a test double that stands in for a client: an error of a status, such as NotFound, given neither a response nor a status, is built with the status x-core raises it for, a ClientError or ServerError with the first of its kind, 400 or 500, and an InvalidResponse with 200. An HTTPError itself, which is raised for a status of any kind, must be given one. A message given is the message of the error, in place of the one read from the body.

Examples:

Create the error of a user that does not exist

error = X::NotFound.new(status: 404, headers: {"content-type" => "application/json"},
  body: %({"title":"Not Found Error","detail":"Could not find user."}))

Raise the error of a rate limit from a test double

raise X::TooManyRequests, "Too Many Requests"

Create an HTTP error from a response

error = X::HTTPError.new(http_response: response, http_method: :get, uri: URI("https://api.x.com/2/users/me"))

Parameters:

  • message (String, nil) (defaults to: nil) —

    the message, or nil for the one the body describes the failure with

  • http_response (Net::HTTPResponse, nil) (defaults to: nil) —

    the HTTP response, or nil for one built of the status, headers, and body

  • status (Integer, nil) (defaults to: nil) —

    the status of the response, from 100 to 599, when it is not given, or nil for the status of the class

  • headers (Hash{String => String}, nil) (defaults to: nil) —

    the headers of the response, when it is not given

  • body (String, nil) (defaults to: nil) —

    the body of the response, when it is not given

  • http_method (Symbol, String, nil) (defaults to: nil) —

    the method of the request the response answers, in any case

  • uri (URI::Generic, nil) (defaults to: nil) —

    the URI of the request the response answers

Raises:

  • (ArgumentError) —

    if the HTTP response is given beside a status, headers, or a body, or neither it nor a status is given to an HTTPError itself, or the status is not from 100 to 599, or the headers are not a Hash of names to values



140
141
142
143
144
145
146
147
148
149
# File 'x-core/lib/x/core/errors/http_error.rb', line 140

def initialize(message = nil, http_response: nil, status: nil, headers: nil, body: nil, http_method: nil, uri: nil)
  @http_response = built_response(http_response, status:, headers:, body:)
  name_request(http_method, uri)
  parsed = parsed_body
  errors = Problem.all_from(parsed)
  described = (Problem.new(parsed) if describes_problem?(parsed))
  @problems = errors.empty? ? [described].compact.freeze : errors
  @problem = described || errors.first
  super(message_naming_request(message || message_from(parsed) || @http_response.message))
end

Instance Attribute Details

#http_response ⇒ Net::HTTPResponse (readonly)

The response itself, as the client received it

It is an escape hatch, for what the error does not read: the status is #status, the headers are #headers, and the body is #body. It is the Net::HTTP response the client sent the request with, or the one built of the status, headers, and body the error was given.

Examples:

Read the reason phrase of the status line

error.http_response.message # => "Too Many Requests"

Returns:

  • (Net::HTTPResponse) —

    the HTTP response



77
78
79
# File 'x-core/lib/x/core/errors/http_error.rb', line 77

def http_response
  @http_response
end

#problem ⇒ Problem? (readonly)

The problem the API described the failure with as a whole

It is the problem the body of the response describes itself, by its title, detail, type, and status, whether or not the body names errors of its own, which are #problems, so its type reads the kind of failure alike for a request the API refused a parameter of and for one it refused to authorize; its attributes are the whole body. A body that describes no problem of its own, but names errors, as the responses of v1.1 do, is described by the first of them.

Examples:

Tell a request the API found invalid from one it refused to authorize

error.problem&.type # => "https://api.twitter.com/2/problems/invalid-request"

Act on the reason rather than the status

wait_for_the_next_month if error.problem&.usage_capped?

Returns:

  • (Problem, nil) —

    the problem, or nil for a response that describes none in JSON



104
105
106
# File 'x-core/lib/x/core/errors/http_error.rb', line 104

def problem
  @problem
end

#problems ⇒ Array<Problem> (readonly)

The problems the API named in the body of the response

They are the errors the body names, such as each parameter of the request the API refused, or else the problem the body describes itself, which is #problem.

Examples:

Name each parameter the API refused

error.problems.map(&:parameter) # => ["ids", "user.fields"]

Returns:

  • (Array<Problem>) —

    the problems, frozen, empty for a response that describes none in JSON



88
89
90
# File 'x-core/lib/x/core/errors/http_error.rb', line 88

def problems
  @problems
end

Instance Method Details

#body ⇒ String?

The body of the response, as it arrived

A server can send any body with an error, so it is the JSON the API describes a failure with, or whatever else was sent in its place, such as the page of a proxy. It is tagged UTF-8, the encoding of the JSON the API sends, and a body that is not valid UTF-8 keeps its bytes, so valid_encoding? tells it apart.

Examples:

Log what the API sent

logger.error(error.body)

Returns:

  • (String, nil) —

    the body, tagged UTF-8, or nil for a response without one



188
# File 'x-core/lib/x/core/errors/http_error.rb', line 188

def body = http_response.body

#headers ⇒ Hash{String => String}

The headers of the response

The names are lowercase, and a field the API sent more than once is joined with a comma.

Examples:

Read how long the API took to answer

error.headers["x-response-time"]

Returns:

  • (Hash{String => String}) —

    the headers, frozen



# File 'x-core/lib/x/core/errors/http_error.rb', line 29

#http_method ⇒ Symbol?

The HTTP method the request was sent with

Examples:

Tell a read that failed from a write

writes_failed += 1 unless error.http_method.eql?(:get)

Returns:

  • (Symbol, nil) —

    the method, as :get, :post, :put, or :delete, or nil for an error built without one



# File 'x-core/lib/x/core/errors/http_error.rb', line 29

#retry_after ⇒ Integer?

The seconds the response asks a request to wait before it is sent again

The API sends a Retry-After header with a request it refused for a rate limit, and with some of the responses of a failure of its own, such as a 503 that names the time its endpoint is expected back. The header counts the seconds from when the response was sent, or names the time to wait until. A client waits it out before it sends an idempotent request again, up to a minute; see Client#initialize.

Examples:

Wait as long as the API asks before sending a request again

sleep(error.retry_after || 1)

Returns:

  • (Integer, nil) —

    the seconds, never negative, or nil for a response that does not say



201
202
203
204
205
206
# File 'x-core/lib/x/core/errors/http_error.rb', line 201

def retry_after
  value = http_response[RETRY_AFTER_HEADER]
  return if value.nil?

  value.match?(RETRY_AFTER_SECONDS) ? Integer(value, 10) : seconds_until(value)
end

#status ⇒ Integer

The HTTP status code, as an Integer like X::Response#status

Examples:

Handle a status the errors do not name

retry if error.status.eql?(408)

Returns:

  • (Integer) —

    the HTTP status code



176
# File 'x-core/lib/x/core/errors/http_error.rb', line 176

def status = Integer(http_response.code)

#uri ⇒ URI::Generic?

The URI the request was sent to

Examples:

Count the failures of each endpoint

failures[error.uri.path] += 1

Returns:

  • (URI::Generic, nil) —

    the URI, or nil for an error built without one



# File 'x-core/lib/x/core/errors/http_error.rb', line 29