Class: Dinie::Internal::HttpClient
- Inherits:
-
Object
- Object
- Dinie::Internal::HttpClient
- Defined in:
- lib/dinie/runtime/http.rb
Overview
‘HttpClient` — the request-lifecycle orchestrator and heart of the runtime (architecture §10, RB15), porting `sdk-js` `src/runtime/http.ts`. It owns one `Faraday::Connection` per Client (adapter `:net_http_persistent` — a real connection pool with keep-alive, thread-safe) and, for every logical request, runs the full lifecycle:
1. mint an `X-Idempotency-Key` ONCE, before the loop, for non-GET writes, so every
retry of the same logical request reuses it (never a duplicate resource);
2. obtain a Bearer token from the injected `TokenManager`;
3. assemble headers (auth, telemetry, idempotency, retry-count, content-type);
4. dispatch through Faraday — **one round-trip per call**; the retry loop is ours;
5. fold `X-RateLimit-*` headers into the snapshot `client.rate_limit` reads;
6. on success (< 300), parse and return the raw body (resources deserialize);
7. on 401, run the one-shot re-auth (`token_manager.invalidate!` + a fresh token), then
give up with `AuthError` if a second 401 follows (no loop);
8. on a retryable status (`{408,429,500,502,503,504}`) or a transient transport error,
back off (`Retry.retry_delay`) and retry while attempts remain, bumping
`X-Dinie-Retry-Count`;
9. otherwise map the response to a typed error via {Errors.from_response}.
── DI seam (architecture §10, RB15) ──The ‘TokenManager` is injected (`HttpClient.new(token_manager:, …)`). Story 004 builds the real, concurrency-safe one; specs inject a fake. `auth_headers` is `token_manager .access_token`; the 401 one-shot is `token_manager.invalidate!`. No global/singleton, no placeholder hack — this is what lets `with_options` (story 004) clone a client while sharing the same token cache.
── controlled runtime → generated seam (openapi-SoT forcing function, architecture §4) ──The rule is “runtime/ never imports generated/”. This module is one declared exception: it references AuthError (forced on a persistent 401) and dispatches non-2xx responses through Errors.from_response, which reads the generated ERROR_REGISTRY. The reference is the forcing function — an error not in ‘openapi.yaml` is not in `generated/`, so there is nowhere to dispatch it.
Runtime-internal: Client and the resources construct and call it; it is not part of the public SDK surface.
The ClassLength cop is disabled below: the request lifecycle (prepare → loop → dispatch →error/retry/re-auth → parse) is one cohesive orchestrator; splitting it across classes would scatter the lifecycle and obscure the parity with ‘sdk-js` `http.ts`.
Constant Summary collapse
- DEFAULT_BASE_URL =
Default production API base URL — carries the ‘/api/v3` version prefix (openapi `servers`). Resource paths are bare (`/customers`), so the version lives here.
"https://api.dinie.com.br/api/v3"- DEFAULT_TIMEOUT_SECONDS =
Default per-request timeout, in seconds (RB17 — Faraday idiom; the TS SDK uses ms).
30- DEFAULT_MAX_RETRIES =
Default retry budget after the first attempt.
3- JSON_CONTENT_TYPE =
‘Content-Type` for JSON request bodies.
"application/json"- AUTO_IDEMPOTENT_METHODS =
Methods that auto-carry an ‘X-Idempotency-Key` when the caller does not say otherwise.
%i[post patch].freeze
- USER_AGENT =
‘User-Agent` sent on every request (architecture §5.1, telemetry AC). The api-version comes from the generated constant Generated::API_VERSION (== openapi info.version).
format( "Dinie-SDK-Ruby/%<sdk>s (api-version=%<api>s; ruby/%<rt>s)", sdk: Dinie::VERSION, api: Dinie::Generated::API_VERSION, rt: RUBY_VERSION ).freeze
Instance Method Summary collapse
-
#initialize(token_manager:, base_url: nil, timeout: DEFAULT_TIMEOUT_SECONDS, max_retries: DEFAULT_MAX_RETRIES, idempotency: true, adapter: nil, connection: nil, sleeper: nil) ⇒ HttpClient
constructor
A new instance of HttpClient.
-
#rate_limit ⇒ Dinie::RateLimit?
Latest rate-limit snapshot (read by ‘client.rate_limit`); `nil` before the first response that carried valid `X-RateLimit-*` headers.
-
#request(method:, path:, query: nil, body: nil, idempotent: nil, request_options: nil) ⇒ Object?
Run one logical request end-to-end and return the parsed body (resources deserialize it).
Constructor Details
#initialize(token_manager:, base_url: nil, timeout: DEFAULT_TIMEOUT_SECONDS, max_retries: DEFAULT_MAX_RETRIES, idempotency: true, adapter: nil, connection: nil, sleeper: nil) ⇒ HttpClient
Returns a new instance of HttpClient.
88 89 90 91 92 93 94 95 96 97 98 99 |
# File 'lib/dinie/runtime/http.rb', line 88 def initialize(token_manager:, base_url: nil, timeout: DEFAULT_TIMEOUT_SECONDS, # rubocop:disable Metrics/ParameterLists max_retries: DEFAULT_MAX_RETRIES, idempotency: true, adapter: nil, connection: nil, sleeper: nil) @token_manager = token_manager @base_url = (base_url || DEFAULT_BASE_URL).sub(%r{/+\z}, "") @timeout = timeout @max_retries = max_retries @idempotency = idempotency @rate_limit_tracker = RateLimitTracker.new @sleeper = sleeper || ->(seconds) { sleep(seconds) } @connection = connection || build_connection(adapter) end |
Instance Method Details
#rate_limit ⇒ Dinie::RateLimit?
Latest rate-limit snapshot (read by ‘client.rate_limit`); `nil` before the first response that carried valid `X-RateLimit-*` headers.
105 106 107 |
# File 'lib/dinie/runtime/http.rb', line 105 def rate_limit @rate_limit_tracker.snapshot end |
#request(method:, path:, query: nil, body: nil, idempotent: nil, request_options: nil) ⇒ Object?
Run one logical request end-to-end and return the parsed body (resources deserialize it). ‘204`/empty bodies return `nil`; a JSON body is parsed with symbol keys; a non-JSON body is returned as raw text.
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 |
# File 'lib/dinie/runtime/http.rb', line 124 def request(method:, path:, query: nil, body: nil, idempotent: nil, request_options: nil) # rubocop:disable Metrics/MethodLength, Metrics/ParameterLists = RequestOptions.coerce() normalized_method = method.to_s.downcase.to_sym serialized_body, content_type = serialize_body(body) execute(PreparedRequest.new( http_method: normalized_method, url: build_full_url(path, query), body: serialized_body, content_type: content_type, idempotency_key: resolve_idempotency_key(idempotent, normalized_method, ), max_retries: .max_retries || @max_retries, timeout: .timeout || @timeout, header_overrides: .headers )) end |