philiprehberger-http_client 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 33e21c126de3c43a3923e7cf7ae975f3fb6c138fe03836236bcab6a83530da94
4
- data.tar.gz: 240a93399f0fc981253749452d52096a4ccaa0b184010831321f7ced135c6463
3
+ metadata.gz: dc47cd54c2873fe7bf5dacad5d62bb028b4ccd9b36e028182e7700cbfeb589de
4
+ data.tar.gz: 26739ada42f419275cada8b35136f0b35fe1c4c2744067466f5e20fc851c4a58
5
5
  SHA512:
6
- metadata.gz: 2137a02d80460a9b6653964dc59fdea65f7c791612c050a83a792fd1db5413e39936852cd49ca0183108d9c9390253d7a176d772607ebc1ddeb3929cc7c14e56
7
- data.tar.gz: 8d0a7b892f1c74f32ad72c7c3da50f790ba503fe3580768dd8e798a0cc46e8089ee29b42afc67ba083be321932fb3dc9e3dc27d73348d1348149f1e1b31110eb
6
+ metadata.gz: d4a8181bf4925a8314d54cbe34db75dd19a79af46d5c7f432bb33172df6c89d1c6b6e0bede1cdbdada2613f8291c1c15a7dee6559ddbf73d432437891ae67531
7
+ data.tar.gz: 48a8ca6c36e77ca87950da44209d7e39eecfe23ee7ed7d9bebf6c63f717c849fb021e69a7aada4a81d52996be7a206c5bff5c53099283abf9cb467f1709907bb
data/CHANGELOG.md CHANGED
@@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.3.1] - 2026-03-12
9
+
10
+ ### Fixed
11
+ - Re-release with no code changes (RubyGems publish fix)
12
+
13
+ ## [0.3.0] - 2026-03-12
14
+
15
+ ### Added
16
+
17
+ - `retry_on_status` option to retry on specific HTTP status codes (e.g., 429, 503)
18
+ - Form-urlencoded body support via `form:` parameter on POST, PUT, and PATCH
19
+ - `bearer_token` helper method for Bearer token authentication
20
+ - `basic_auth` helper method for HTTP Basic authentication
21
+
22
+ ### Fixed
23
+
24
+ - Exponential backoff delay: first retry now uses base delay instead of 2x base delay
25
+
8
26
  ## [0.2.0] - 2026-03-12
9
27
 
10
28
  ### Added
data/README.md CHANGED
@@ -76,6 +76,25 @@ client.get("/health")
76
76
  # Response: 200
77
77
  ```
78
78
 
79
+ ### Form data
80
+
81
+ ```ruby
82
+ response = client.post("/login", form: { username: "alice", password: "secret" })
83
+ # Content-Type: application/x-www-form-urlencoded is set automatically
84
+ ```
85
+
86
+ ### Authentication helpers
87
+
88
+ ```ruby
89
+ # Bearer token
90
+ client.bearer_token("your-api-token")
91
+ client.get("/protected") # Authorization: Bearer your-api-token
92
+
93
+ # Basic auth
94
+ client.basic_auth("username", "password")
95
+ client.get("/protected") # Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=
96
+ ```
97
+
79
98
  ### Retries
80
99
 
81
100
  Automatically retry on network errors (connection refused, timeouts, etc.):
@@ -90,6 +109,19 @@ client = Philiprehberger::HttpClient.new(
90
109
  response = client.get("/unstable-endpoint")
91
110
  ```
92
111
 
112
+ You can also retry on specific HTTP status codes:
113
+
114
+ ```ruby
115
+ client = Philiprehberger::HttpClient.new(
116
+ base_url: "https://api.example.com",
117
+ retries: 3,
118
+ retry_delay: 1,
119
+ retry_on_status: [429, 503]
120
+ )
121
+ ```
122
+
123
+ > **Note:** Network error retries and HTTP status retries both count toward the same retry limit.
124
+
93
125
  ### Exponential backoff
94
126
 
95
127
  ```ruby
@@ -99,6 +131,7 @@ client = Philiprehberger::HttpClient.new(
99
131
  retry_delay: 1,
100
132
  retry_backoff: :exponential
101
133
  )
134
+ # Delay sequence: 1s, 2s, 4s
102
135
  ```
103
136
 
104
137
  ### Per-request timeout
@@ -129,7 +162,8 @@ client.head("/resource")
129
162
  | `timeout` | Integer | `30` | Read/open timeout in seconds |
130
163
  | `retries` | Integer | `0` | Retry attempts on network errors |
131
164
  | `retry_delay` | Numeric | `1` | Seconds between retries |
132
- | `retry_backoff` | String/Symbol | `:fixed` | Backoff strategy — `:fixed` or `:exponential` |
165
+ | `retry_backoff` | Symbol | `:fixed` | Backoff strategy — `:fixed` or `:exponential` |
166
+ | `retry_on_status` | Array | `nil` | HTTP status codes to retry on (e.g., `[429, 503]`) |
133
167
 
134
168
  ### Methods
135
169
 
@@ -142,6 +176,8 @@ client.head("/resource")
142
176
  | `delete(path, **opts)` | Send DELETE request |
143
177
  | `head(path, **opts)` | Send HEAD request |
144
178
  | `request_count` | Total number of requests made by this client |
179
+ | `bearer_token(token)` | Set Bearer token auth for all subsequent requests |
180
+ | `basic_auth(user, pass)` | Set Basic auth for all subsequent requests |
145
181
 
146
182
  ### `Response`
147
183
 
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "base64"
3
4
  require "net/http"
4
5
  require "uri"
5
6
  require "json"
@@ -22,6 +23,7 @@ module Philiprehberger
22
23
  @retries = retry_options.fetch(:retries, 0)
23
24
  @retry_delay = retry_options.fetch(:retry_delay, 1)
24
25
  @retry_backoff = retry_options.fetch(:retry_backoff, :fixed)
26
+ @retry_on_status = retry_options[:retry_on_status]
25
27
  @interceptors = []
26
28
  @request_count = 0
27
29
  end
@@ -75,13 +77,11 @@ module Philiprehberger
75
77
  # @param path [String] Request path
76
78
  # @param body [String, nil] Raw body string
77
79
  # @param json [Hash, Array, nil] JSON-serializable body (sets Content-Type automatically)
80
+ # @param form [Hash, nil] Form-urlencoded body (sets Content-Type automatically)
78
81
  # @param headers [Hash] Additional headers
79
82
  # @return [Response]
80
- def post(path, body: nil, json: nil, headers: {}, timeout: nil)
81
- uri = build_uri(path)
82
- request = Net::HTTP::Post.new(uri)
83
- set_body(request, body, json, headers)
84
- execute(uri, request, headers, timeout: timeout)
83
+ def post(path, **opts)
84
+ request_with_body(Net::HTTP::Post, path, **opts)
85
85
  end
86
86
 
87
87
  # Perform a PUT request.
@@ -89,13 +89,11 @@ module Philiprehberger
89
89
  # @param path [String] Request path
90
90
  # @param body [String, nil] Raw body string
91
91
  # @param json [Hash, Array, nil] JSON-serializable body
92
+ # @param form [Hash, nil] Form-urlencoded body
92
93
  # @param headers [Hash] Additional headers
93
94
  # @return [Response]
94
- def put(path, body: nil, json: nil, headers: {}, timeout: nil)
95
- uri = build_uri(path)
96
- request = Net::HTTP::Put.new(uri)
97
- set_body(request, body, json, headers)
98
- execute(uri, request, headers, timeout: timeout)
95
+ def put(path, **opts)
96
+ request_with_body(Net::HTTP::Put, path, **opts)
99
97
  end
100
98
 
101
99
  # Perform a PATCH request.
@@ -103,13 +101,11 @@ module Philiprehberger
103
101
  # @param path [String] Request path
104
102
  # @param body [String, nil] Raw body string
105
103
  # @param json [Hash, Array, nil] JSON-serializable body
104
+ # @param form [Hash, nil] Form-urlencoded body
106
105
  # @param headers [Hash] Additional headers
107
106
  # @return [Response]
108
- def patch(path, body: nil, json: nil, headers: {}, timeout: nil)
109
- uri = build_uri(path)
110
- request = Net::HTTP::Patch.new(uri)
111
- set_body(request, body, json, headers)
112
- execute(uri, request, headers, timeout: timeout)
107
+ def patch(path, **opts)
108
+ request_with_body(Net::HTTP::Patch, path, **opts)
113
109
  end
114
110
 
115
111
  # Perform a DELETE request.
@@ -122,6 +118,26 @@ module Philiprehberger
122
118
  request = Net::HTTP::Delete.new(uri)
123
119
  execute(uri, request, headers, timeout: timeout)
124
120
  end
121
+
122
+ # Set a Bearer token for all subsequent requests.
123
+ #
124
+ # @param token [String] the bearer token
125
+ # @return [self]
126
+ def bearer_token(token)
127
+ @default_headers["authorization"] = "Bearer #{token}"
128
+ self
129
+ end
130
+
131
+ # Set Basic auth credentials for all subsequent requests.
132
+ #
133
+ # @param username [String]
134
+ # @param password [String]
135
+ # @return [self]
136
+ def basic_auth(username, password)
137
+ encoded = Base64.strict_encode64("#{username}:#{password}")
138
+ @default_headers["authorization"] = "Basic #{encoded}"
139
+ self
140
+ end
125
141
  end
126
142
  end
127
143
  end
@@ -5,8 +5,21 @@ module Philiprehberger
5
5
  # Internal helpers for building URIs, HTTP connections, executing requests,
6
6
  # and constructing Response objects. Mixed into Client to keep it concise.
7
7
  module Connection
8
+ RETRYABLE_ERRORS = [
9
+ Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::ETIMEDOUT,
10
+ Net::OpenTimeout, Net::ReadTimeout, SocketError
11
+ ].freeze
12
+
8
13
  private
9
14
 
15
+ def request_with_body(http_class, path, **opts)
16
+ headers = opts.fetch(:headers, {})
17
+ uri = build_uri(path)
18
+ request = http_class.new(uri)
19
+ set_body(request, opts[:body], opts[:json], opts[:form], headers)
20
+ execute(uri, request, headers, timeout: opts[:timeout])
21
+ end
22
+
10
23
  def build_uri(path, params = {})
11
24
  url = "#{@base_url}/#{path.sub(%r{^/}, '')}"
12
25
  uri = URI.parse(url)
@@ -17,10 +30,13 @@ module Philiprehberger
17
30
  uri
18
31
  end
19
32
 
20
- def set_body(request, body, json_body, headers)
33
+ def set_body(request, body, json_body, form_body, headers)
21
34
  if json_body
22
35
  request.body = JSON.generate(json_body)
23
36
  headers["content-type"] ||= "application/json"
37
+ elsif form_body
38
+ request.body = URI.encode_www_form(form_body)
39
+ headers["content-type"] ||= "application/x-www-form-urlencoded"
24
40
  elsif body
25
41
  request.body = body
26
42
  end
@@ -47,20 +63,28 @@ module Philiprehberger
47
63
 
48
64
  def perform_with_retries(uri, request, timeout: nil)
49
65
  attempts = 0
50
- begin
51
- perform_request(uri, request, timeout: timeout)
52
- rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::ETIMEDOUT,
53
- Net::OpenTimeout, Net::ReadTimeout, SocketError => e
54
- attempts += 1
55
- raise e unless attempts <= @retries
66
+ loop do
67
+ response = perform_request(uri, request, timeout: timeout)
68
+ return response unless retry_on_status?(response.status, attempts)
69
+
70
+ wait_and_retry(attempts += 1)
71
+ rescue *RETRYABLE_ERRORS => e
72
+ raise e unless (attempts += 1) <= @retries
56
73
 
57
74
  sleep(retry_delay_for(attempts))
58
- retry
59
75
  end
60
76
  end
61
77
 
78
+ def retry_on_status?(status, attempts)
79
+ @retry_on_status&.include?(status) && attempts < @retries
80
+ end
81
+
82
+ def wait_and_retry(attempt)
83
+ sleep(retry_delay_for(attempt))
84
+ end
85
+
62
86
  def retry_delay_for(attempt)
63
- @retry_backoff == :exponential ? @retry_delay * (2**attempt) : @retry_delay
87
+ @retry_backoff == :exponential ? @retry_delay * (2**(attempt - 1)) : @retry_delay
64
88
  end
65
89
 
66
90
  def perform_request(uri, request, timeout: nil)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Philiprehberger
4
4
  module HttpClient
5
- VERSION = "0.2.0"
5
+ VERSION = "0.3.1"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: philiprehberger-http_client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Philip Rehberger