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