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 +4 -4
- data/CHANGELOG.md +18 -0
- data/README.md +37 -1
- data/lib/philiprehberger/http_client/client.rb +31 -15
- data/lib/philiprehberger/http_client/connection.rb +33 -9
- data/lib/philiprehberger/http_client/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dc47cd54c2873fe7bf5dacad5d62bb028b4ccd9b36e028182e7700cbfeb589de
|
|
4
|
+
data.tar.gz: 26739ada42f419275cada8b35136f0b35fe1c4c2744067466f5e20fc851c4a58
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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` |
|
|
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,
|
|
81
|
-
|
|
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,
|
|
95
|
-
|
|
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,
|
|
109
|
-
|
|
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
|
-
|
|
51
|
-
perform_request(uri, request, timeout: timeout)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
attempts += 1
|
|
55
|
-
|
|
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)
|