Class: ConvertSdk::HttpClient

Inherits:
Object
  • Object
show all
Defined in:
lib/convert_sdk/http_client.rb

Overview

The single hardened HTTP port every SDK request flows through.

HttpClient is the only file in the gem that touches Net::HTTP (a cheap architectural regression test asserts this). Every request it sends carries the ConvertAgent wire invariant and bounded timeouts, and every failure it encounters is converted into a failed Response rather than raised — the port NEVER raises to callers, so the config fetch (Story 2.5) and event delivery (Story 4.1) consumers degrade gracefully on a failed response.

The ConvertAgent wire invariant

The metrics endpoint's bot filter silently DROPS server-side events whose User-Agent is not ConvertAgent/1.0. The header is therefore applied LAST, after every header merge, so it cannot be overridden by an integrator-supplied User-Agent. Without it, tracking events would vanish silently in production. (JS/PHP precedent: set unconditionally after merge.)

Bounded timeouts

Both open_timeout and read_timeout are set explicitly on EVERY request (a deliberate improvement over the JS SDK, which sets none). The SDK can never hang a host thread waiting on a slow or dead endpoint.

TLS / Bearer / proxies

HTTPS endpoints use TLS with verification ON (+verify_mode+ is never VERIFY_NONE). An Authorization: Bearer ... header is stripped (and a warning logged) on any non-HTTPS endpoint so the SDK key secret never crosses the wire in plaintext. Proxies are honoured through the standard Net::HTTP environment conventions (+http_proxy+/+https_proxy+/+no_proxy+).

JSON boundary

Callers pass and receive Ruby hashes; JSON encode/decode happens only here. A request body hash is rendered with JSON.generate; a response body is parsed with JSON.parse (string keys). A parse failure is logged and yields body: nil on an otherwise intact response.

All logging goes through the injected LogManager (never puts), so the Redactor masks secrets and strips URL query strings from every line.

Defined Under Namespace

Classes: Response

Constant Summary collapse

USER_AGENT =

The mandatory wire User-Agent. Applied LAST so it is unoverridable.

"ConvertAgent/1.0"
FAILURE_STATUS =

The status used for a failed Response when no HTTP response was received (network error / timeout). Callers MUST use Response#success?, never compare the status integer, for error detection.

0

Instance Method Summary collapse

Constructor Details

#initialize(log_manager:, open_timeout:, read_timeout:) ⇒ HttpClient

Returns a new instance of HttpClient.

Parameters:

  • log_manager (LogManager)

    the injected logging surface. All output flows through it so the Redactor applies.

  • open_timeout (Numeric)

    connection-establishment timeout (seconds).

  • read_timeout (Numeric)

    response-read timeout (seconds).



86
87
88
89
90
# File 'lib/convert_sdk/http_client.rb', line 86

def initialize(log_manager:, open_timeout:, read_timeout:)
  @log_manager = log_manager
  @open_timeout = open_timeout
  @read_timeout = read_timeout
end

Instance Method Details

#request(method:, url:, headers: {}, body: nil) ⇒ Response

Send one HTTP request and return a frozen Response. Never raises: any transport failure is logged and returned as a failed response.

Parameters:

  • method (Symbol)

    :get / :post / etc.

  • url (String)

    the absolute request URL.

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

    caller headers (merged before the wire invariant is applied last).

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

    a request body; JSON-encoded if present.

Returns:

  • (Response)

    frozen; success? is the only valid error check.



101
102
103
104
105
106
107
108
109
110
111
# File 'lib/convert_sdk/http_client.rb', line 101

def request(method:, url:, headers: {}, body: nil)
  uri = URI.parse(url)
  https = uri.scheme == "https"
  wire_headers = build_headers(headers, https)
  @log_manager.debug("HttpClient#request: #{method.to_s.upcase} #{url}")

  perform(method, uri, https, wire_headers, body)
rescue StandardError => e
  @log_manager.error("HttpClient#request: request failed (#{e.class}: #{e.message})")
  failed_response
end