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 +4 -4
- data/CHANGELOG.md +16 -0
- data/README.md +22 -0
- data/lib/philiprehberger/http_client/client.rb +46 -0
- data/lib/philiprehberger/http_client/connection.rb +15 -1
- data/lib/philiprehberger/http_client/errors.rb +3 -0
- data/lib/philiprehberger/http_client/response.rb +3 -3
- data/lib/philiprehberger/http_client/version.rb +1 -1
- data/lib/philiprehberger/http_client.rb +9 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8c45ebe7d84b21537896e6e973ed05aa04a2d582f51e3ffe84f75be8896aba31
|
|
4
|
+
data.tar.gz: 3113777f92b0047542c4498885e259fcf87823ccbecb0bc993aa0de99f0c9b54
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
|
|
54
|
-
@redirects || []
|
|
55
|
-
end
|
|
55
|
+
attr_reader :redirects
|
|
56
56
|
|
|
57
57
|
# Returns true if the response was redirected.
|
|
58
58
|
#
|
|
@@ -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.
|
|
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-
|
|
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.
|