Class: Hook0::Transport

Inherits:
Object
  • Object
show all
Defined in:
lib/hook0/transport.rb

Overview

How a request reaches the API, and what a server on the other end is not allowed to cost.

The transport answers the status and the bytes and knows nothing of what the API declares: reading those bytes is the generated half's job, and deciding whether to send them again is the client's. That is what lets one HTTP implementation serve both the hand-written event path and every generated method — a generated group calls whatever object it is handed, and this is the one this gem ships.

Nothing here reaches for a third-party HTTP library. Everything a server controls is bounded: how long one exchange may take, and how many bytes of body are read off the socket.

Constant Summary collapse

DEFAULT_REQUEST_TIMEOUT =

Longest one attempt at reaching the API is given before it is abandoned, in seconds.

Ten seconds is far above what ingesting an event takes when the API is healthy, and short enough that a stuck connection does not hold a caller for a noticeable time.

10.0
DEFAULT_MAX_RESPONSE_BYTES =

Largest response body read off a socket, in bytes.

8 * 1024 * 1024
DEFAULT_MAX_RESPONSE_HEADERS =

How many header lines an answer may carry before it is refused.

Net::HTTP bounds neither how many header lines it accepts nor how long one may be: it holds fifty thousand of them, and a single value of eight megabytes, without complaint. So the head of an answer is a server-controlled way to spend a caller's memory, and the ceiling has to be this client's own. Sixty-four is well above what the API sends.

64
DEFAULT_MAX_HEADER_BYTES =

Longest one header line may be, name and value together, in bytes.

64 * 1024
DEFAULT_MAX_HEAD_BYTES =

Largest whole head an answer may carry, every line counted together, in bytes.

This is the one that bounds what a head costs. A line count and a size per line multiply: sixty-four lines of sixty-four kilobytes each is four megabytes of head, and both of the bounds above admit it. They earn their place by refusing early, on the line that crosses them rather than at the end of the head; this one sets the ceiling.

Sixteen kilobytes is what Node enforces by default, and matching it is the point: a lower ceiling would refuse heads another target accepts, and a higher one would not bind there at all, leaving each language a different effective limit.

16 * 1024
JSON_MEDIA_TYPE =

What a request body says it carries, and what an answer is asked for in.

"application/json"
SCHEMES =

The schemes this transport reaches.

%w[http https].freeze
UNREACHABLE =

What the standard library reports when the API was not reached at all.

[
  IOError,
  SocketError,
  SystemCallError,
  Timeout::Error,
  Net::HTTPBadResponse,
  Net::HTTPHeaderSyntaxError,
  Net::ProtocolError,
  OpenSSL::SSL::SSLError
].freeze

Instance Method Summary collapse

Constructor Details

#initialize(base_url, token, timeout: DEFAULT_REQUEST_TIMEOUT, max_response_bytes: DEFAULT_MAX_RESPONSE_BYTES, max_response_headers: DEFAULT_MAX_RESPONSE_HEADERS, max_header_bytes: DEFAULT_MAX_HEADER_BYTES, max_head_bytes: DEFAULT_MAX_HEAD_BYTES, retry_policy: RetryPolicy.new) ⇒ Transport

Returns a new instance of Transport.

Parameters:

  • base_url (String)

    where the API lives, such as https://app.hook0.com/api/v1

  • token (String)

    an authentication token valid for that API

  • timeout (Float) (defaults to: DEFAULT_REQUEST_TIMEOUT)

    how long one attempt is given, in seconds

  • max_response_bytes (Integer) (defaults to: DEFAULT_MAX_RESPONSE_BYTES)

    the largest answer read off a socket

  • max_response_headers (Integer) (defaults to: DEFAULT_MAX_RESPONSE_HEADERS)

    how many header lines an answer may carry

  • max_header_bytes (Integer) (defaults to: DEFAULT_MAX_HEADER_BYTES)

    the longest one header line may be

  • max_head_bytes (Integer) (defaults to: DEFAULT_MAX_HEAD_BYTES)

    the largest whole head, every line counted together

  • retry_policy (RetryPolicy) (defaults to: RetryPolicy.new)

    the policy every request states the client was built with



170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/hook0/transport.rb', line 170

def initialize(
  base_url,
  token,
  timeout: DEFAULT_REQUEST_TIMEOUT,
  max_response_bytes: DEFAULT_MAX_RESPONSE_BYTES,
  max_response_headers: DEFAULT_MAX_RESPONSE_HEADERS,
  max_header_bytes: DEFAULT_MAX_HEADER_BYTES,
  max_head_bytes: DEFAULT_MAX_HEAD_BYTES,
  retry_policy: RetryPolicy.new
)
  @base_url = base_url
  @token = token
  @timeout = timeout
  @max_response_bytes = max_response_bytes
  @max_response_headers = max_response_headers
  @max_header_bytes = max_header_bytes
  @max_head_bytes = max_head_bytes
  @retry_policy = retry_policy
end

Instance Method Details

#deliver(method, path, query = [], body = nil) ⇒ Array(Integer, Hash{String => String}, String)

What the API answered, headers included, whether or not it answered a success.

Header names are lowercased and a later value wins over an earlier one under the same name, so a caller reads a header without knowing which case the server wrote it in.

Parameters:

  • method (String)

    the HTTP method the operation is issued under

  • path (String)

    where the request lands, absolute or under the base URL

  • query (Array<Array<String>>) (defaults to: [])

    the name and value pairs of the query string

  • body (Object, nil) (defaults to: nil)

    what to send as a JSON document, or nothing at all

Returns:

  • (Array(Integer, Hash{String => String}, String))

    the status, the headers and the body

Raises:



218
219
220
221
# File 'lib/hook0/transport.rb', line 218

def deliver(method, path, query = [], body = nil)
  target = resolved(path, query)
  exchange(target, built(method, target, body))
end

#request(method, path, query = [], body = nil) ⇒ Array(Integer, String)

What the API answered, whether or not it answered a success.

This is the shape the generated half of this gem reads, which is the status and the bytes. A caller that also needs what the answer carried beside its body — the delay a paced instance names is one — asks #deliver for it.

Parameters:

  • method (String)

    the HTTP method the operation is issued under

  • path (String)

    where the request lands, absolute or under the base URL

  • query (Array<Array<String>>) (defaults to: [])

    the name and value pairs of the query string

  • body (Object, nil) (defaults to: nil)

    what to send as a JSON document, or nothing at all

Returns:

  • (Array(Integer, String))

    the status and the body

Raises:



202
203
204
205
# File 'lib/hook0/transport.rb', line 202

def request(method, path, query = [], body = nil)
  status, _, payload = deliver(method, path, query, body)
  [status, payload]
end