Class: Axene::Mailer::Transport

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

Overview

HTTP transport: the single place that talks to the network. Owns bearer authentication, JSON encode/decode, timeouts, retries with backoff, and turning non-2xx responses into Error. Resources depend on this, not on Net::HTTP directly.

Constant Summary collapse

DEFAULT_BASE_URL =
"https://mail.axene.io"
USER_AGENT =
"axene-mailer-ruby/#{VERSION}"

Instance Method Summary collapse

Constructor Details

#initialize(api_key:, base_url: DEFAULT_BASE_URL, max_retries: 3, timeout: 30) ⇒ Transport

Returns a new instance of Transport.

Parameters:

  • api_key (String)

    required; starts with “axm_k_”

  • base_url (String) (defaults to: DEFAULT_BASE_URL)

    default “mail.axene.io

  • max_retries (Integer) (defaults to: 3)

    retry attempts for 429/5xx, default 3

  • timeout (Numeric) (defaults to: 30)

    per-request timeout in seconds, default 30

Raises:

  • (ArgumentError)


24
25
26
27
28
29
30
31
# File 'lib/axene/mailer/transport.rb', line 24

def initialize(api_key:, base_url: DEFAULT_BASE_URL, max_retries: 3, timeout: 30)
  raise ArgumentError, "Axene::Mailer: `api_key` is required." if api_key.nil? || api_key.empty?

  @api_key = api_key
  @base_url = base_url.sub(%r{/+\z}, "")
  @max_retries = max_retries
  @timeout = timeout
end

Instance Method Details

#request(method, path, body: nil, query: nil) ⇒ Hash, ...

Perform a JSON request and return the parsed body (symbolized keys).

Retries 429 and 5xx with exponential backoff, honoring Retry-After when present. Raises Error on a final non-2xx or a transport failure that survives every attempt.

Parameters:

  • method (Symbol)

    :get, :post, :patch, :put, :delete

  • path (String)

    path beginning with “/”

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

    request body, JSON-encoded when present

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

    query parameters (nil values dropped)

Returns:

  • (Hash, Array, nil)

Raises:



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/axene/mailer/transport.rb', line 44

def request(method, path, body: nil, query: nil)
  uri = build_uri(path, query)
  last_error = nil

  (1..@max_retries).each do |attempt|
    req = build_request(method, uri)
    unless body.nil?
      req["Content-Type"] = "application/json"
      req.body = JSON.generate(body)
    end

    begin
      res = http(uri).request(req)
    rescue StandardError => e
      last_error = e
      sleep(backoff_seconds(nil, attempt)) if attempt < @max_retries
      next
    end

    status = res.code.to_i
    if retryable?(status) && attempt < @max_retries
      sleep(backoff_seconds(res, attempt))
      next
    end

    payload = parse_body(res)
    raise to_error(status, payload) unless status.between?(200, 299)

    return payload
  end

  raise Error.new(0, "Axene::Mailer request failed: #{last_error}")
end

#upload(path, file_bytes, filename) ⇒ Hash, ...

Upload a single file as multipart/form-data under the field name “file”. Used by the CSV/suppression import endpoints. Not retried (uploads are not idempotent).

Parameters:

  • path (String)
  • file_bytes (String)

    raw file contents

  • filename (String)

Returns:

  • (Hash, Array, nil)


86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/axene/mailer/transport.rb', line 86

def upload(path, file_bytes, filename)
  uri = build_uri(path, nil)
  boundary = "AxeneBoundary#{SecureRandom.hex(16)}"
  req = build_request(:post, uri)
  req["Content-Type"] = "multipart/form-data; boundary=#{boundary}"
  req.body = multipart_body(boundary, file_bytes, filename)

  res = http(uri).request(req)
  status = res.code.to_i
  payload = parse_body(res)
  raise to_error(status, payload) unless status.between?(200, 299)

  payload
end