philiprehberger-http_client 0.7.0 → 0.8.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: 0d0ec8636cc91257698783cc3876cb93be649c33ad70c91317c6738b145a2ab1
4
- data.tar.gz: 778941d598bfb51d3ec2e1291e8ab4231ac43137385659b17795bdebf8f14c21
3
+ metadata.gz: 8c45ebe7d84b21537896e6e973ed05aa04a2d582f51e3ffe84f75be8896aba31
4
+ data.tar.gz: 3113777f92b0047542c4498885e259fcf87823ccbecb0bc993aa0de99f0c9b54
5
5
  SHA512:
6
- metadata.gz: a86d3da1c7d89b7bbf1341cb0efe062f99e12a025909f28307bfc163f24435f1706a2b1d2a025439b2ba8d59060c9b06c04fe97b213895a5587ba0ecd1c9da4b
7
- data.tar.gz: 46e39334ae5e243f378ca80e31313180f980bf819fcce88760d3c3b8a2628fc0cd23b99219da31e56d520364dfca0fd9f643ab9cbf0a0c907780e85f6c017494
6
+ metadata.gz: 8ad3b396d9ab5ad1c004a3363b57108f51709318171a848fb31f167faab95ebbb1ca5827510d86248ffa3b6e1aa37a10024eeb7678ac8a4b553548db7f7612bd
7
+ data.tar.gz: 733c04bde42f3ff1a987c77b4221a236c595cd07447be80fb195626f029ddf08d822b1f6868a18bd414bf45755b36ebf6f00d80bad6e997175a0f114889d24cd
data/CHANGELOG.md CHANGED
@@ -7,6 +7,22 @@ and this gem adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.8.1] - 2026-04-07
11
+
12
+ ### Added
13
+ - `ConfigurationError` raised at initialization for invalid `timeout`, `open_timeout`, `read_timeout`, `write_timeout`, `retries`, `retry_delay`, `retry_backoff`, or `retry_on_status` values — fail fast instead of producing confusing runtime errors
14
+
15
+ ## [0.8.0] - 2026-04-05
16
+
17
+ ### Added
18
+ - `Client#close` method to explicitly drain the connection pool
19
+ - `Client.open(**opts, &block)` block form with automatic cleanup
20
+
21
+ ### Fixed
22
+ - Response objects now properly initialize `metrics` and `redirects` attributes
23
+ - 307 and 308 redirects now preserve the original HTTP method instead of converting to GET
24
+ - Clarified that `dns_time`, `connect_time`, and `tls_time` metrics are unavailable from Net::HTTP
25
+
10
26
  ## [0.7.0] - 2026-04-04
11
27
 
12
28
  ### Added
data/README.md CHANGED
@@ -299,6 +299,8 @@ metrics.first_byte_time # => 0.180
299
299
  metrics.to_h # => { dns_time: 0.0, connect_time: 0.0, ... }
300
300
  ```
301
301
 
302
+ > **Note:** `dns_time`, `connect_time`, and `tls_time` are not available from Ruby's stdlib `Net::HTTP` and will always be `0.0`. Only `total_time` and `first_byte_time` are populated.
303
+
302
304
  ### Connection pooling
303
305
 
304
306
  Reuse TCP connections to the same host for better performance:
@@ -317,6 +319,23 @@ client = Philiprehberger::HttpClient.new(
317
319
  client.pool.drain
318
320
  ```
319
321
 
322
+ ### Client lifecycle
323
+
324
+ Use `Client.open` for automatic cleanup, or call `close` manually to drain the connection pool:
325
+
326
+ ```ruby
327
+ # Block form — pool is drained automatically
328
+ Philiprehberger::HttpClient.open(base_url: "https://api.example.com", pool: true) do |client|
329
+ client.get("/data")
330
+ client.post("/submit", json: { key: "value" })
331
+ end
332
+
333
+ # Manual form
334
+ client = Philiprehberger::HttpClient.new(base_url: "https://api.example.com", pool: true)
335
+ client.get("/data")
336
+ client.close # drains the connection pool
337
+ ```
338
+
320
339
  ### Request ID tracking
321
340
 
322
341
  Every request is assigned a unique `X-Request-ID` header automatically:
@@ -428,6 +447,8 @@ client.head("/resource")
428
447
  | `bearer_token(token)` | Set Bearer token auth for all subsequent requests |
429
448
  | `basic_auth(user, pass)` | Set Basic auth for all subsequent requests |
430
449
  | `clear_cache!` | Flush the response cache |
450
+ | `close` | Drain the connection pool (no-op if pooling disabled) |
451
+ | `self.open(**opts, &block)` | Block form — creates client, yields it, ensures `close` is called |
431
452
  | `pool` | Returns the `Pool` instance (nil if pooling disabled) |
432
453
  | `cache` | Returns the `Cache` instance (nil if caching disabled) |
433
454
 
@@ -468,6 +489,7 @@ client.head("/resource")
468
489
  | Class | Description |
469
490
  |-------|-------------|
470
491
  | `Error` | Base error class (inherits `StandardError`) |
492
+ | `ConfigurationError` | Invalid client option (negative timeout, bad retry config, etc.) |
471
493
  | `TimeoutError` | Connection or read timeout |
472
494
  | `NetworkError` | Connection refused, reset, unreachable |
473
495
  | `HttpError` | Response status mismatch (has `.response` accessor) |
@@ -32,8 +32,10 @@ module Philiprehberger
32
32
  @base_url = base_url.chomp('/')
33
33
  @default_headers = headers
34
34
  @timeout = timeout
35
+ validate_timeout!(:timeout, timeout)
35
36
  assign_timeout_opts(opts)
36
37
  assign_retry_opts(opts)
38
+ validate_config!
37
39
  assign_cookie_opts(opts)
38
40
  assign_proxy_opts(opts)
39
41
  assign_redirect_opts(opts)
@@ -204,12 +206,56 @@ module Philiprehberger
204
206
  @cache&.clear!
205
207
  end
206
208
 
209
+ # Drain the connection pool. No-op if pooling is disabled.
210
+ #
211
+ # @return [void]
212
+ def close
213
+ @pool&.drain
214
+ end
215
+
216
+ # Create a client, yield it to the block, and ensure it is closed afterward.
217
+ #
218
+ # @param opts [Hash] Options forwarded to {#initialize}
219
+ # @yield [Client] the client instance
220
+ # @return [Object] the return value of the block
221
+ def self.open(**opts)
222
+ client = new(**opts)
223
+ yield client
224
+ ensure
225
+ client&.close
226
+ end
227
+
207
228
  private
208
229
 
209
230
  def assign_timeout_opts(opts)
210
231
  @open_timeout = opts[:open_timeout]
211
232
  @read_timeout = opts[:read_timeout]
212
233
  @write_timeout = opts[:write_timeout]
234
+ validate_timeout!(:open_timeout, @open_timeout)
235
+ validate_timeout!(:read_timeout, @read_timeout)
236
+ validate_timeout!(:write_timeout, @write_timeout)
237
+ end
238
+
239
+ def validate_timeout!(name, value)
240
+ return if value.nil?
241
+ raise ConfigurationError, "#{name} must be Numeric, got #{value.class}" unless value.is_a?(Numeric)
242
+ raise ConfigurationError, "#{name} must be non-negative, got #{value}" if value.negative?
243
+ end
244
+
245
+ def validate_config!
246
+ unless @retries.is_a?(Integer) && !@retries.negative?
247
+ raise ConfigurationError, "retries must be a non-negative Integer, got #{@retries.inspect}"
248
+ end
249
+ unless @retry_delay.is_a?(Numeric) && !@retry_delay.negative?
250
+ raise ConfigurationError, "retry_delay must be a non-negative Numeric, got #{@retry_delay.inspect}"
251
+ end
252
+ unless %i[fixed exponential].include?(@retry_backoff)
253
+ raise ConfigurationError, "retry_backoff must be :fixed or :exponential, got #{@retry_backoff.inspect}"
254
+ end
255
+ return if @retry_on_status.nil?
256
+ return if @retry_on_status.respond_to?(:include?)
257
+
258
+ raise ConfigurationError, 'retry_on_status must be an Array of status codes'
213
259
  end
214
260
 
215
261
  def assign_retry_opts(opts)
@@ -185,6 +185,15 @@ module Philiprehberger
185
185
  @follow_redirects && REDIRECT_CODES.include?(response.status)
186
186
  end
187
187
 
188
+ METHOD_CLASS_MAP = {
189
+ 'GET' => Net::HTTP::Get,
190
+ 'POST' => Net::HTTP::Post,
191
+ 'PUT' => Net::HTTP::Put,
192
+ 'PATCH' => Net::HTTP::Patch,
193
+ 'DELETE' => Net::HTTP::Delete,
194
+ 'HEAD' => Net::HTTP::Head
195
+ }.freeze
196
+
188
197
  def follow_redirect_chain(response, original_request, **timeout_opts)
189
198
  redirect_count = 0
190
199
  redirects = []
@@ -199,7 +208,12 @@ module Philiprehberger
199
208
  redirect_uri = URI.parse(location)
200
209
  redirect_uri = URI.join("#{original_request.uri.scheme}://#{original_request.uri.host}", location) unless redirect_uri.host
201
210
 
202
- redirect_request = Net::HTTP::Get.new(redirect_uri)
211
+ redirect_class = if [307, 308].include?(current_response.status)
212
+ METHOD_CLASS_MAP.fetch(original_request.method, Net::HTTP::Get)
213
+ else
214
+ Net::HTTP::Get
215
+ end
216
+ redirect_request = redirect_class.new(redirect_uri)
203
217
  @default_headers.each { |key, value| redirect_request[key] = value }
204
218
  redirect_request['accept-encoding'] ||= 'gzip, deflate'
205
219
  apply_cookie_header(redirect_request)
@@ -5,6 +5,9 @@ module Philiprehberger
5
5
  # Base error class for all HTTP client errors.
6
6
  class Error < StandardError; end
7
7
 
8
+ # Raised when client configuration is invalid (e.g. negative timeout).
9
+ class ConfigurationError < Error; end
10
+
8
11
  # Raised when a connection or read timeout occurs.
9
12
  class TimeoutError < Error; end
10
13
 
@@ -18,6 +18,8 @@ module Philiprehberger
18
18
  @headers = headers
19
19
  @streaming = streaming
20
20
  @request_id = request_id
21
+ @metrics = nil
22
+ @redirects = []
21
23
  end
22
24
 
23
25
  # Returns true if the status code is in the 2xx range.
@@ -50,9 +52,7 @@ module Philiprehberger
50
52
  # Returns the redirect chain (empty if no redirects occurred).
51
53
  #
52
54
  # @return [Array<String>]
53
- def redirects
54
- @redirects || []
55
- end
55
+ attr_reader :redirects
56
56
 
57
57
  # Returns true if the response was redirected.
58
58
  #
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Philiprehberger
4
4
  module HttpClient
5
- VERSION = '0.7.0'
5
+ VERSION = '0.8.1'
6
6
  end
7
7
  end
@@ -22,5 +22,14 @@ module Philiprehberger
22
22
  def self.new(**options)
23
23
  Client.new(**options)
24
24
  end
25
+
26
+ # Block form — creates a client, yields it, and ensures cleanup.
27
+ #
28
+ # @param options [Hash] Forwarded to {Client#initialize}
29
+ # @yield [Client]
30
+ # @return [Object] the return value of the block
31
+ def self.open(...)
32
+ Client.open(...)
33
+ end
25
34
  end
26
35
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: philiprehberger-http_client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.8.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Philip Rehberger
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-04-05 00:00:00.000000000 Z
11
+ date: 2026-04-07 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: A zero-dependency HTTP client built on Ruby's net/http with automatic
14
14
  retries, request/response interceptors, and a clean API for JSON services.