Class: Basecamp::Http
- Inherits:
-
Object
- Object
- Basecamp::Http
- Defined in:
- lib/basecamp/http.rb
Overview
HTTP client layer with retry, backoff, and caching support. This is an internal class used by Client; you typically don't use it directly.
Constant Summary collapse
- USER_AGENT =
Default User-Agent header
"basecamp-sdk-ruby/#{VERSION} (api:#{API_VERSION})".freeze
- DOWNLOAD_RETRY_ON =
SPEC §14's declared hop-1 retry set for downloads: a carve-out from the ungoverned GET taxonomy (which retries all retryable 5xx, including 500). Authoritative in BOTH directions, like an operation's declared retryOn.
[ 429, 502, 503, 504 ].freeze
Class Method Summary collapse
-
.normalize_person_ids(obj) ⇒ Object
Normalizes Person-shaped objects in parsed JSON.
-
.operation_retry(operation) ⇒ Object
Memoized per-operation retry metadata, keyed by canonical operation ID.
Instance Method Summary collapse
-
#base_url ⇒ String
The configured base URL.
-
#delete(path) ⇒ Response
Performs a DELETE request.
-
#get(path, params: {}, operation: nil) ⇒ Response
Performs a GET request.
-
#get_absolute(url, params: {}) ⇒ Response
Performs a GET request to an absolute URL.
-
#get_authorization_document ⇒ Response
Fetches the credentialed authorization document (the fixed
authorization.jsonpath). -
#get_download(url) ⇒ Response
Performs the authenticated hop-1 GET for the download flow (SPEC §14).
-
#initialize(config:, token_provider: nil, auth_strategy: nil, hooks: nil) ⇒ Http
constructor
A new instance of Http.
-
#paginate(path, params: {}, operation: nil, max_items: nil) {|Hash| ... } ⇒ ListEnumerator
Fetches all pages of a paginated resource.
-
#paginate_key(path, key:, params: {}, operation: nil, max_items: nil) {|Hash| ... } ⇒ ListEnumerator
Fetches all pages of a paginated resource, extracting items from a key.
-
#paginate_wrapped(path, key:, params: {}, operation: nil, max_items: nil) ⇒ Hash
Fetches a wrapped paginated resource, returning wrapper fields + lazy paginated items.
-
#post(path, body: nil) ⇒ Response
Performs a POST request.
-
#post_raw(path, body:, content_type:) ⇒ Response
Performs a POST request with raw binary data.
-
#put(path, body: nil) ⇒ Response
Performs a PUT request.
-
#put_raw(path, body:, content_type:) ⇒ Response
Performs a PUT request with raw binary data.
Constructor Details
#initialize(config:, token_provider: nil, auth_strategy: nil, hooks: nil) ⇒ Http
Returns a new instance of Http.
42 43 44 45 46 47 48 |
# File 'lib/basecamp/http.rb', line 42 def initialize(config:, token_provider: nil, auth_strategy: nil, hooks: nil) @config = config @auth_strategy = auth_strategy || BearerAuth.new(token_provider) @token_provider = token_provider || (@auth_strategy.is_a?(BearerAuth) ? @auth_strategy.token_provider : nil) @hooks = hooks || NoopHooks.new @faraday = build_faraday_client end |
Class Method Details
.normalize_person_ids(obj) ⇒ Object
Normalizes Person-shaped objects in parsed JSON. For objects with personable_type and a string id:
- Numeric strings: coerced to Integer, no system_label
- Non-numeric sentinels (e.g. "basecamp"): id becomes 0, system_label preserves original
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
# File 'lib/basecamp/http.rb', line 19 def self.normalize_person_ids(obj) case obj when Hash if obj.key?("personable_type") && obj["id"].is_a?(String) raw_id = obj["id"] numeric = Integer(raw_id, exception: false) if numeric obj["id"] = numeric else obj["system_label"] = raw_id obj["id"] = 0 end end obj.each_value { |v| normalize_person_ids(v) } when Array obj.each { |item| normalize_person_ids(item) } end end |
.operation_retry(operation) ⇒ Object
Memoized per-operation retry metadata, keyed by canonical operation ID. Benign-race memoization: concurrent first loads compute identical values.
495 496 497 498 499 500 |
# File 'lib/basecamp/http.rb', line 495 def self.operation_retry(operation) @operation_metadata ||= JSON.parse( File.read(File.join(__dir__, "generated", "metadata.json")) ).fetch("operations").freeze @operation_metadata.dig(operation, "retry") end |
Instance Method Details
#base_url ⇒ String
Returns the configured base URL.
51 52 53 |
# File 'lib/basecamp/http.rb', line 51 def base_url @config.base_url end |
#delete(path) ⇒ Response
Performs a DELETE request.
138 139 140 |
# File 'lib/basecamp/http.rb', line 138 def delete(path) request(:delete, path) end |
#get(path, params: {}, operation: nil) ⇒ Response
Performs a GET request.
62 63 64 |
# File 'lib/basecamp/http.rb', line 62 def get(path, params: {}, operation: nil) request(:get, path, params: params, operation: operation) end |
#get_absolute(url, params: {}) ⇒ Response
Performs a GET request to an absolute URL. Used for endpoints not on the base API.
This is the PUBLIC, general path and it credentials cross-origin for ONE
destination only: the exact Launchpad authorization URL
(Security::LAUNCHPAD_AUTHORIZATION_URL). Every other foreign
origin — including an endpoint-shaped URL such as
https://evil.example/authorization.json — trips the same-origin guard, so
the bearer token only ever reaches Launchpad, the configured base URL, or
localhost. There is deliberately NO raw-string trusted-origin parameter: a
syntactically valid origin does not prove discovery provenance, so the ONE
legitimate cross-origin discovery destination goes through the narrow
#get_authorization_document, which derives its issuer from internal
discovery of the configured base URL rather than any caller argument.
84 85 86 87 88 89 |
# File 'lib/basecamp/http.rb', line 84 def get_absolute(url, params: {}) Security.require_https_unless_localhost!(url, "absolute URL") allow_cross_origin = url == Security::LAUNCHPAD_AUTHORIZATION_URL request(:get, url, params: params, allow_cross_origin: allow_cross_origin) end |
#get_authorization_document ⇒ Response
Fetches the credentialed authorization document (the fixed authorization.json
path). This is the ONE sanctioned cross-origin credential path besides
Launchpad, and the origin that receives the bearer token is NOT
caller-supplied.
The issuer is derived HERE by running resource-first discovery (SPEC.md §16) against this client's OWN configured base URL, then binding to whatever issuer discovery selects and validates (RFC 8414 issuer binding). A soft fallback fetches Launchpad's fixed URL; a hard discovery failure raises. The request URL is CONSTRUCTED from the discovered issuer origin + the fixed path (string concatenation, never URL re-parsing). Because no caller-supplied config, origin, or path reaches this method, there is no public API through which a forged issuer could redirect the credential to a foreign host — discovery provenance is structural, not a claim about a passed-in object.
109 110 111 112 113 114 115 116 117 |
# File 'lib/basecamp/http.rb', line 109 def result = Oauth.discover_from_resource(@config.base_url) if result.selected? issuer_origin = Security.require_origin_root!(result.issuer, "selected issuer origin") request(:get, "#{issuer_origin}/authorization.json", allow_cross_origin: true) else get_absolute(Security::LAUNCHPAD_AUTHORIZATION_URL) end end |
#get_download(url) ⇒ Response
Performs the authenticated hop-1 GET for the download flow (SPEC §14).
Retries network errors plus the declared DOWNLOAD_RETRY_ON statuses — never 500 — under the public max_retries total-attempt cap, which is floored at one attempt on every path, not just this one (+max_retries: 0+ still sends one request). DownloadURL has no behavior-model entry, so the policy is passed directly rather than looked up by operation.
178 179 180 |
# File 'lib/basecamp/http.rb', line 178 def get_download(url) request_with_retry(:get, url, retry_on: DOWNLOAD_RETRY_ON, accept: nil) end |
#paginate(path, params: {}, operation: nil, max_items: nil) {|Hash| ... } ⇒ ListEnumerator
Fetches all pages of a paginated resource. The first page is fetched eagerly, so pagination metadata (and any page-1 error) surfaces at call time; later pages are fetched lazily as enumeration demands them.
192 193 194 195 |
# File 'lib/basecamp/http.rb', line 192 def paginate(path, params: {}, operation: nil, max_items: nil, &block) enum = paginated_enumerator(path, params: params, operation: operation, max_items: max_items) block ? enum.each(&block) : enum end |
#paginate_key(path, key:, params: {}, operation: nil, max_items: nil) {|Hash| ... } ⇒ ListEnumerator
Fetches all pages of a paginated resource, extracting items from a key. Use this for endpoints that return objects like { "events": [...] }. The first page is fetched eagerly; later pages are fetched lazily.
207 208 209 210 |
# File 'lib/basecamp/http.rb', line 207 def paginate_key(path, key:, params: {}, operation: nil, max_items: nil, &block) enum = paginated_enumerator(path, key: key, params: params, operation: operation, max_items: max_items) block ? enum.each(&block) : enum end |
#paginate_wrapped(path, key:, params: {}, operation: nil, max_items: nil) ⇒ Hash
Fetches a wrapped paginated resource, returning wrapper fields + lazy paginated items. Use this for endpoints that return ..., key: [items] on every page.
220 221 222 223 224 225 226 227 |
# File 'lib/basecamp/http.rb', line 220 def paginate_wrapped(path, key:, params: {}, operation: nil, max_items: nil) wrapper = nil events = paginated_enumerator(path, key: key, params: params, operation: operation, \ max_items: max_items) do |first_data| wrapper = first_data.reject { |k, _| k == key } end wrapper.merge(key => events) end |
#post(path, body: nil) ⇒ Response
Performs a POST request.
123 124 125 |
# File 'lib/basecamp/http.rb', line 123 def post(path, body: nil) request(:post, path, body: body) end |
#post_raw(path, body:, content_type:) ⇒ Response
Performs a POST request with raw binary data. Used for file uploads (attachments).
148 149 150 151 |
# File 'lib/basecamp/http.rb', line 148 def post_raw(path, body:, content_type:) url = build_url(path) single_request_raw(:post, url, body: body, content_type: content_type, attempt: 1) end |
#put(path, body: nil) ⇒ Response
Performs a PUT request.
131 132 133 |
# File 'lib/basecamp/http.rb', line 131 def put(path, body: nil) request(:put, path, body: body) end |
#put_raw(path, body:, content_type:) ⇒ Response
Performs a PUT request with raw binary data. Used for multipart uploads (e.g., account logo).
159 160 161 162 |
# File 'lib/basecamp/http.rb', line 159 def put_raw(path, body:, content_type:) url = build_url(path) single_request_raw(:put, url, body: body, content_type: content_type, attempt: 1) end |