nxt_http_client 2.1.1 → 2.2.0
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/.github/workflows/ci.yml +30 -0
- data/.ruby-version +1 -1
- data/CHANGELOG.md +20 -0
- data/Gemfile.lock +47 -41
- data/README.md +81 -6
- data/lib/nxt_http_client/client.rb +25 -4
- data/lib/nxt_http_client/client_dsl.rb +18 -0
- data/lib/nxt_http_client/config.rb +4 -1
- data/lib/nxt_http_client/error.rb +87 -2
- data/lib/nxt_http_client/version.rb +1 -1
- metadata +4 -8
- data/.circleci/config.yml +0 -20
- data/.travis.yml +0 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b2c6e9588a07ff6820f7988990ce130bbe7ad8b0cc3467b83b5222e7a7061ed3
|
|
4
|
+
data.tar.gz: 6bf74b2f2c1788cc1140fa63444238a04a55130fe1bdd2ac6d1621d941aad22f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 479a2c6036211c446ccab839f750aba66b3d10df80676eca0134f23be474798f65b1524e38e8e85c834b2e353c92f4561140b7190d41849227a9f175a9915381
|
|
7
|
+
data.tar.gz: a7694a6c37e29420f042b0bc65588586783fee0e4509771accb72e4f3cd2dc204992b05d98604288546a4351c45cb361d711bebdeffef514c52fb47e21c20615
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
tests:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
strategy:
|
|
15
|
+
fail-fast: false
|
|
16
|
+
matrix:
|
|
17
|
+
ruby-version: ["3.3", "3.4", "4.0"]
|
|
18
|
+
services:
|
|
19
|
+
redis:
|
|
20
|
+
image: redis:7
|
|
21
|
+
ports:
|
|
22
|
+
- 6379:6379
|
|
23
|
+
options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v6
|
|
26
|
+
- uses: ruby/setup-ruby@v1
|
|
27
|
+
with:
|
|
28
|
+
ruby-version: ${{ matrix.ruby-version }}
|
|
29
|
+
bundler-cache: true
|
|
30
|
+
- run: bundle exec rspec
|
data/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
4.0.5
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
# v2.2.0 2026-06-15
|
|
2
|
+
- Report HTTP error details to Sentry via the structured `set_context('http_error', …)` instead of the
|
|
3
|
+
deprecated `set_extras(http_error_details: …)`. (Typhoeus is not auto-instrumented by Sentry, so the gem
|
|
4
|
+
attaches this itself.)
|
|
5
|
+
- `Error#to_h` now redacts the `Authorization` header and basic-auth `userpwd` (it is sent to Sentry).
|
|
6
|
+
- `json_response` now returns `nil` for an empty/204 body instead of raising `JSON::ParserError`.
|
|
7
|
+
- Add an opt-in error taxonomy under `NxtHttpClient::Error`. With `config.raise_error_taxonomy = true` the client
|
|
8
|
+
raises a typed subclass for an unhandled 4xx/5xx/code-0 response instead of returning it:
|
|
9
|
+
- HTTP status: `ClientError` with `BadRequest` (400), `Unauthorized` (401), `Forbidden` (403),
|
|
10
|
+
`NotFound` (404), `UnprocessableEntity` (422), `TooManyRequests` (429); `ServerError` (5xx).
|
|
11
|
+
- Network (`return_code`-mapped code-0): `NetworkError` with `Timeout`, `ConnectionFailed`,
|
|
12
|
+
`NameResolutionError`, `TlsError`; `CertificateError` (cert verification — a sibling, not a child).
|
|
13
|
+
- Retryable errors share base classes — `retry_on NxtHttpClient::Error::NetworkError, NxtHttpClient::Error::ServerError`.
|
|
14
|
+
4xx, `CertificateError` and 429 are excluded (429 retry policy is left to consumers).
|
|
15
|
+
- `map_error(status, klass)` DSL to override the mapping per client (e.g. a domain `ValidationFailed` that
|
|
16
|
+
parses the body); inherited by subclasses.
|
|
17
|
+
- `config.raise_error_taxonomy` defaults to `false`, so the upgrade is backwards compatible — existing behavior
|
|
18
|
+
(and `raise_response_errors`) is unchanged until you opt in. A consumer's own `on(<code>)`/`on(:error)`/
|
|
19
|
+
`on(:timed_out)` callback always takes precedence over the taxonomy.
|
|
20
|
+
|
|
1
21
|
# v2.1.0 2024-06-05
|
|
2
22
|
- Bump dependencies
|
|
3
23
|
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
nxt_http_client (2.
|
|
4
|
+
nxt_http_client (2.2.0)
|
|
5
5
|
activesupport
|
|
6
6
|
nxt_registry
|
|
7
7
|
typhoeus
|
|
@@ -9,84 +9,90 @@ PATH
|
|
|
9
9
|
GEM
|
|
10
10
|
remote: https://rubygems.org/
|
|
11
11
|
specs:
|
|
12
|
-
activesupport (8.
|
|
12
|
+
activesupport (8.1.3)
|
|
13
13
|
base64
|
|
14
|
-
benchmark (>= 0.3)
|
|
15
14
|
bigdecimal
|
|
16
15
|
concurrent-ruby (~> 1.0, >= 1.3.1)
|
|
17
16
|
connection_pool (>= 2.2.5)
|
|
18
17
|
drb
|
|
19
18
|
i18n (>= 1.6, < 2)
|
|
19
|
+
json
|
|
20
20
|
logger (>= 1.4.2)
|
|
21
21
|
minitest (>= 5.1)
|
|
22
22
|
securerandom (>= 0.3)
|
|
23
23
|
tzinfo (~> 2.0, >= 2.0.5)
|
|
24
24
|
uri (>= 0.13.1)
|
|
25
|
-
addressable (2.
|
|
26
|
-
public_suffix (>= 2.0.2, <
|
|
27
|
-
base64 (0.
|
|
28
|
-
|
|
29
|
-
bigdecimal (3.1.8)
|
|
25
|
+
addressable (2.9.0)
|
|
26
|
+
public_suffix (>= 2.0.2, < 8.0)
|
|
27
|
+
base64 (0.3.0)
|
|
28
|
+
bigdecimal (4.1.2)
|
|
30
29
|
coderay (1.1.3)
|
|
31
|
-
concurrent-ruby (1.3.
|
|
32
|
-
connection_pool (
|
|
33
|
-
crack (1.0.
|
|
30
|
+
concurrent-ruby (1.3.6)
|
|
31
|
+
connection_pool (3.0.2)
|
|
32
|
+
crack (1.0.1)
|
|
34
33
|
bigdecimal
|
|
35
34
|
rexml
|
|
36
|
-
diff-lcs (1.
|
|
37
|
-
drb (2.2.
|
|
38
|
-
ethon (0.
|
|
35
|
+
diff-lcs (1.6.2)
|
|
36
|
+
drb (2.2.3)
|
|
37
|
+
ethon (0.18.0)
|
|
39
38
|
ffi (>= 1.15.0)
|
|
40
|
-
|
|
41
|
-
ffi (1.17.
|
|
42
|
-
ffi (1.17.
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
logger
|
|
40
|
+
ffi (1.17.4)
|
|
41
|
+
ffi (1.17.4-x86_64-darwin)
|
|
42
|
+
ffi (1.17.4-x86_64-linux-gnu)
|
|
43
|
+
hashdiff (1.2.1)
|
|
44
|
+
i18n (1.14.8)
|
|
45
45
|
concurrent-ruby (~> 1.0)
|
|
46
|
-
|
|
46
|
+
io-console (0.8.2)
|
|
47
|
+
json (2.19.8)
|
|
48
|
+
logger (1.7.0)
|
|
47
49
|
method_source (1.1.0)
|
|
48
|
-
minitest (
|
|
50
|
+
minitest (6.0.6)
|
|
51
|
+
drb (~> 2.0)
|
|
52
|
+
prism (~> 1.5)
|
|
49
53
|
nxt_registry (0.3.10)
|
|
50
54
|
activesupport
|
|
51
55
|
nxt_vcr_harness (0.2.0)
|
|
52
56
|
rspec (~> 3.0)
|
|
53
57
|
vcr (~> 6.0)
|
|
54
|
-
|
|
58
|
+
prism (1.9.0)
|
|
59
|
+
pry (0.16.0)
|
|
55
60
|
coderay (~> 1.1)
|
|
56
61
|
method_source (~> 1.0)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
62
|
+
reline (>= 0.6.0)
|
|
63
|
+
public_suffix (7.0.5)
|
|
64
|
+
rake (13.4.2)
|
|
65
|
+
redis (5.4.1)
|
|
60
66
|
redis-client (>= 0.22.0)
|
|
61
|
-
redis-client (0.
|
|
67
|
+
redis-client (0.29.0)
|
|
62
68
|
connection_pool
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
69
|
+
reline (0.6.3)
|
|
70
|
+
io-console (~> 0.5)
|
|
71
|
+
rexml (3.4.4)
|
|
72
|
+
rspec (3.13.2)
|
|
66
73
|
rspec-core (~> 3.13.0)
|
|
67
74
|
rspec-expectations (~> 3.13.0)
|
|
68
75
|
rspec-mocks (~> 3.13.0)
|
|
69
|
-
rspec-core (3.13.
|
|
76
|
+
rspec-core (3.13.6)
|
|
70
77
|
rspec-support (~> 3.13.0)
|
|
71
|
-
rspec-expectations (3.13.
|
|
78
|
+
rspec-expectations (3.13.5)
|
|
72
79
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
73
80
|
rspec-support (~> 3.13.0)
|
|
74
|
-
rspec-mocks (3.13.
|
|
81
|
+
rspec-mocks (3.13.8)
|
|
75
82
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
76
83
|
rspec-support (~> 3.13.0)
|
|
77
|
-
rspec-support (3.13.
|
|
84
|
+
rspec-support (3.13.7)
|
|
78
85
|
rspec_junit_formatter (0.6.0)
|
|
79
86
|
rspec-core (>= 2, < 4, != 2.12.0)
|
|
80
|
-
securerandom (0.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
ethon (>= 0.9.0)
|
|
87
|
+
securerandom (0.4.1)
|
|
88
|
+
timecop (0.9.11)
|
|
89
|
+
typhoeus (1.6.0)
|
|
90
|
+
ethon (>= 0.18.0)
|
|
85
91
|
tzinfo (2.0.6)
|
|
86
92
|
concurrent-ruby (~> 1.0)
|
|
87
|
-
uri (1.
|
|
88
|
-
vcr (6.
|
|
89
|
-
webmock (3.
|
|
93
|
+
uri (1.1.1)
|
|
94
|
+
vcr (6.4.0)
|
|
95
|
+
webmock (3.26.2)
|
|
90
96
|
addressable (>= 2.8.0)
|
|
91
97
|
crack (>= 0.3.2)
|
|
92
98
|
hashdiff (>= 0.4.0, < 2.0.0)
|
data/README.md
CHANGED
|
@@ -48,7 +48,7 @@ class UserServiceClient < NxtHttpClient::Client
|
|
|
48
48
|
# Note: This error handler is set by default when you use
|
|
49
49
|
# config.raise_response_errors = true
|
|
50
50
|
handler.on(:error) do |response|
|
|
51
|
-
Sentry.
|
|
51
|
+
Sentry.set_context('http_error', error.to_h)
|
|
52
52
|
raise StandardError, "I can't handle this: #{response.code}"
|
|
53
53
|
end
|
|
54
54
|
end
|
|
@@ -118,12 +118,16 @@ Register your default request options on the class level. Available options are:
|
|
|
118
118
|
- `base_url=`
|
|
119
119
|
- `x_request_id_proc=`
|
|
120
120
|
- `json_request=`: Shorthand to set the Content-Type request header to JSON and automatically convert request bodies to JSON
|
|
121
|
-
- `json_response=`: Shorthand to set the Accept request header and automatically convert success response bodies to JSON
|
|
122
|
-
- `raise_response_errors=`: Makes the client raise a `NxtHttpClient::Error` for a non-success response.
|
|
123
|
-
|
|
121
|
+
- `json_response=`: Shorthand to set the Accept request header and automatically convert success response bodies to JSON (an empty/204 body becomes `nil`)
|
|
122
|
+
- `raise_response_errors=`: Makes the client raise a generic `NxtHttpClient::Error` for a non-success response.
|
|
123
|
+
Superseded by `raise_error_taxonomy` (which raises typed errors and takes precedence when both are set); kept for
|
|
124
|
+
backward compatibility.
|
|
125
|
+
- `raise_error_taxonomy=`: Defaults to `false`. Opt in to raise the mapped `NxtHttpClient::Error` taxonomy
|
|
126
|
+
(`ClientError`/`ServerError`/`NetworkError` subclasses) on an unhandled 4xx/5xx/code-0 response instead of
|
|
127
|
+
returning it. See [Error taxonomy](#error-taxonomy).
|
|
124
128
|
- `bearer_auth=`: Set a bearer token to be sent in the Authorization header
|
|
125
129
|
- `basic_auth=`: Pass a Hash containing `:username` and `:password`, to be sent as Basic credentials in the Authorization header
|
|
126
|
-
- `
|
|
130
|
+
- `timeout_seconds(total:, connect: nil)`: Configure timeouts
|
|
127
131
|
|
|
128
132
|
### response_handler
|
|
129
133
|
|
|
@@ -215,13 +219,84 @@ To set a timeout, use the `timeout_seconds` config method:
|
|
|
215
219
|
configure do |config|
|
|
216
220
|
config.timeout_seconds(total: 10)
|
|
217
221
|
# You can also set a connect timeout
|
|
218
|
-
config.timeout_seconds(total: 10,
|
|
222
|
+
config.timeout_seconds(total: 10, connect: 2)
|
|
219
223
|
end
|
|
220
224
|
```
|
|
221
225
|
|
|
222
226
|
NxtHttpClient::Error exposes the `timed_out?` method from `Typhoeus::Response`, so you can check if an error is raised due to a timeout.
|
|
223
227
|
This is useful when setting a custom timeout value in your configuration.
|
|
224
228
|
|
|
229
|
+
### Error taxonomy
|
|
230
|
+
|
|
231
|
+
Set `config.raise_error_taxonomy = true` and the client raises a typed subclass of `NxtHttpClient::Error` for an
|
|
232
|
+
unhandled 4xx, 5xx, or code-0 (network) response, so you no longer need to hand-roll per-status
|
|
233
|
+
`on(400)`/`on(422)`/`on(5xx)`/`on(0)` handlers just to get a usable taxonomy. (3xx and other codes are returned
|
|
234
|
+
as before.) It is **off by default** (the client returns the response); all classes inherit from
|
|
235
|
+
`NxtHttpClient::Error`, so existing `rescue NxtHttpClient::Error` handlers keep working.
|
|
236
|
+
|
|
237
|
+
**HTTP status:**
|
|
238
|
+
|
|
239
|
+
| status | class | retryable? |
|
|
240
|
+
|---------------|---------------------------------------------|------------|
|
|
241
|
+
| 400 | `NxtHttpClient::Error::BadRequest` | no |
|
|
242
|
+
| 401 | `NxtHttpClient::Error::Unauthorized` | no |
|
|
243
|
+
| 403 | `NxtHttpClient::Error::Forbidden` | no |
|
|
244
|
+
| 404 | `NxtHttpClient::Error::NotFound` | no |
|
|
245
|
+
| 422 | `NxtHttpClient::Error::UnprocessableEntity` | no |
|
|
246
|
+
| 429 | `NxtHttpClient::Error::TooManyRequests` | up to you |
|
|
247
|
+
| other 4xx | `NxtHttpClient::Error::ClientError` | no |
|
|
248
|
+
| 5xx | `NxtHttpClient::Error::ServerError` | yes |
|
|
249
|
+
|
|
250
|
+
**Network / code 0.** Typhoeus/libcurl surfaces network failures and timeouts as a response with HTTP **code 0**
|
|
251
|
+
(no response received); the real cause lives in libcurl's `return_code`:
|
|
252
|
+
|
|
253
|
+
| libcurl `return_code` | class | retryable? |
|
|
254
|
+
|----------------------------------------------------|---------------------------------------------|------------|
|
|
255
|
+
| `:operation_timedout` | `NxtHttpClient::Error::Timeout` | yes |
|
|
256
|
+
| `:couldnt_connect` | `NxtHttpClient::Error::ConnectionFailed` | yes |
|
|
257
|
+
| `:couldnt_resolve_host` / `:couldnt_resolve_proxy` | `NxtHttpClient::Error::NameResolutionError` | yes |
|
|
258
|
+
| `:ssl_connect_error` and other non-cert `:ssl_*` | `NxtHttpClient::Error::TlsError` | yes |
|
|
259
|
+
| any other code-0 | `NxtHttpClient::Error::NetworkError` | yes |
|
|
260
|
+
| cert verification (`:peer_failed_verification`, …) | `NxtHttpClient::Error::CertificateError` | no |
|
|
261
|
+
|
|
262
|
+
#### Retrying
|
|
263
|
+
|
|
264
|
+
The retryable errors share two base classes, so a job retries them in one place:
|
|
265
|
+
|
|
266
|
+
```ruby
|
|
267
|
+
retry_on NxtHttpClient::Error::NetworkError, NxtHttpClient::Error::ServerError
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
`ClientError` (4xx) is not retried — those are caller mistakes. `CertificateError` is a **sibling** of `NetworkError`
|
|
271
|
+
(not a child), so it is excluded from `retry_on NetworkError` — a failed cert/CA verification is permanent.
|
|
272
|
+
`TooManyRequests` (429) is left out of the retryable set; add your own `retry_on NxtHttpClient::Error::TooManyRequests`
|
|
273
|
+
(ideally honoring `Retry-After`) if you want it.
|
|
274
|
+
|
|
275
|
+
#### Precedence and opting out
|
|
276
|
+
|
|
277
|
+
A consumer's own `on(<code>)` / `on(:error)` / `on(:timed_out)` callback always takes precedence; the raise only
|
|
278
|
+
fires when nothing else handled the response. So you can enable the taxonomy for retries yet still handle, say, a
|
|
279
|
+
404 inline with your own `on(404)`.
|
|
280
|
+
|
|
281
|
+
#### Domain-typed errors
|
|
282
|
+
|
|
283
|
+
Map a status to your own error class with `map_error` to get a domain error that parses the response body. It is
|
|
284
|
+
inherited by subclasses and overrides the default for that status. `map_error` only takes effect when
|
|
285
|
+
`raise_error_taxonomy` is enabled — it customizes the taxonomy, it does not enable raising on its own:
|
|
286
|
+
|
|
287
|
+
```ruby
|
|
288
|
+
class MyService::Client < NxtHttpClient::Client
|
|
289
|
+
configure { |config| config.raise_error_taxonomy = true }
|
|
290
|
+
map_error 422, MyService::Error::ValidationFailed
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
class MyService::Error::ValidationFailed < NxtHttpClient::Error
|
|
294
|
+
def default_message
|
|
295
|
+
body.dig('errors', 0, 'detail') # `body` parses JSON response bodies for you
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
```
|
|
299
|
+
|
|
225
300
|
### Logging
|
|
226
301
|
|
|
227
302
|
NxtHttpClient also comes with a log method on the class level that you can pass a proc if you want to log your request.
|
|
@@ -144,7 +144,25 @@ module NxtHttpClient
|
|
|
144
144
|
|
|
145
145
|
def callback_or_response(response, response_handler)
|
|
146
146
|
callback = response_handler.callback_for_response(response)
|
|
147
|
-
|
|
147
|
+
return instance_exec(response, &callback) || response if callback
|
|
148
|
+
|
|
149
|
+
return raise_mapped_error(response) if raise_mapped_error?(response)
|
|
150
|
+
|
|
151
|
+
response
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Reached only when no consumer callback matched, so a consumer on(<code>)/on(:error) keeps precedence.
|
|
155
|
+
def raise_mapped_error?(response)
|
|
156
|
+
return false unless config.raise_error_taxonomy
|
|
157
|
+
|
|
158
|
+
code = response.code.to_i
|
|
159
|
+
code.zero? || (400..599).cover?(code)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def raise_mapped_error(response)
|
|
163
|
+
error = self.class.error_class_for(response).new(response)
|
|
164
|
+
::Sentry.set_context('http_error', error.to_h) if defined?(::Sentry)
|
|
165
|
+
raise error
|
|
148
166
|
end
|
|
149
167
|
|
|
150
168
|
def build_response_handler(handler, &block)
|
|
@@ -153,17 +171,20 @@ module NxtHttpClient
|
|
|
153
171
|
if config.json_response
|
|
154
172
|
response_handler.configure do |handler|
|
|
155
173
|
handler.on(:success) do |response|
|
|
156
|
-
|
|
174
|
+
# nil for a blank/204 body — parsing "" would raise JSON::ParserError
|
|
175
|
+
response.define_singleton_method(:body) { response.response_body.presence && JSON(response.response_body) }
|
|
157
176
|
response
|
|
158
177
|
end
|
|
159
178
|
end
|
|
160
179
|
end
|
|
161
180
|
|
|
162
|
-
|
|
181
|
+
# Legacy generic-error raising. Superseded by raise_error_taxonomy (typed), which takes precedence here
|
|
182
|
+
# via the callback_or_response fallback — so don't also register this shadowing on(:error) handler.
|
|
183
|
+
if config.raise_response_errors && !config.raise_error_taxonomy
|
|
163
184
|
response_handler.configure do |handler|
|
|
164
185
|
handler.on(:error) do |response|
|
|
165
186
|
error = NxtHttpClient::Error.new(response)
|
|
166
|
-
::Sentry.
|
|
187
|
+
::Sentry.set_context('http_error', error.to_h) if defined?(::Sentry)
|
|
167
188
|
raise error
|
|
168
189
|
end
|
|
169
190
|
end
|
|
@@ -52,6 +52,24 @@ module NxtHttpClient
|
|
|
52
52
|
@response_handler
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
+
# Override the taxonomy's error class for a status, e.g. `map_error 422, MyService::ValidationFailed`.
|
|
56
|
+
# Only takes effect when config.raise_error_taxonomy is enabled.
|
|
57
|
+
def map_error(status, error_class)
|
|
58
|
+
unless error_class.is_a?(Class) && error_class <= NxtHttpClient::Error
|
|
59
|
+
raise ArgumentError, "#{error_class.inspect} must be a subclass of NxtHttpClient::Error"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
error_map[Integer(status)] = error_class
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def error_map
|
|
66
|
+
@error_map ||= dup_option_from_ancestor(:@error_map) { {} }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def error_class_for(response)
|
|
70
|
+
error_map[response.code.to_i] || NxtHttpClient::Error.error_class_for(response)
|
|
71
|
+
end
|
|
72
|
+
|
|
55
73
|
private
|
|
56
74
|
|
|
57
75
|
def client_ancestors
|
|
@@ -8,7 +8,10 @@ module NxtHttpClient
|
|
|
8
8
|
json_request: false,
|
|
9
9
|
# Helper to set the Accept request header and automatically convert success response bodies to JSON
|
|
10
10
|
json_response: false,
|
|
11
|
-
raise_response_errors: false,
|
|
11
|
+
raise_response_errors: false, # Legacy: raises a generic NxtHttpClient::Error. Superseded by raise_error_taxonomy.
|
|
12
|
+
# Opt in (default false) to raise the mapped NxtHttpClient::Error taxonomy on an unhandled
|
|
13
|
+
# 4xx/5xx/code-0 response instead of returning it. See README "Error taxonomy".
|
|
14
|
+
raise_error_taxonomy: false,
|
|
12
15
|
|
|
13
16
|
bearer_auth: nil,
|
|
14
17
|
basic_auth: nil,
|
|
@@ -1,5 +1,49 @@
|
|
|
1
1
|
module NxtHttpClient
|
|
2
2
|
class Error < StandardError
|
|
3
|
+
# Cert/trust failures — mapped to non-retryable CertificateError, kept out of the generic TlsError.
|
|
4
|
+
CERTIFICATE_RETURN_CODES = %i[
|
|
5
|
+
peer_failed_verification ssl_certproblem ssl_cacert_badfile ssl_issuer_error ssl_crl_badfile
|
|
6
|
+
].freeze
|
|
7
|
+
|
|
8
|
+
REDACTED = '[REDACTED]'
|
|
9
|
+
SENSITIVE_HEADERS = %w[Authorization Proxy-Authorization].freeze
|
|
10
|
+
|
|
11
|
+
def self.from_response(response, message = nil)
|
|
12
|
+
error_class_for(response).new(response, message)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.error_class_for(response)
|
|
16
|
+
code = response.respond_to?(:code) ? response.code.to_i : 0
|
|
17
|
+
return network_error_class(response.respond_to?(:return_code) ? response.return_code : nil) if code.zero?
|
|
18
|
+
|
|
19
|
+
status_error_class(code)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.status_error_class(code)
|
|
23
|
+
case code
|
|
24
|
+
when 400 then BadRequest
|
|
25
|
+
when 401 then Unauthorized
|
|
26
|
+
when 403 then Forbidden
|
|
27
|
+
when 404 then NotFound
|
|
28
|
+
when 422 then UnprocessableEntity
|
|
29
|
+
when 429 then TooManyRequests
|
|
30
|
+
when 400..499 then ClientError
|
|
31
|
+
when 500..599 then ServerError
|
|
32
|
+
else self # 3xx etc. → base Error
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.network_error_class(return_code)
|
|
37
|
+
case return_code
|
|
38
|
+
when :operation_timedout then Timeout
|
|
39
|
+
when :couldnt_connect then ConnectionFailed
|
|
40
|
+
when :couldnt_resolve_host, :couldnt_resolve_proxy then NameResolutionError
|
|
41
|
+
when *CERTIFICATE_RETURN_CODES then CertificateError
|
|
42
|
+
else
|
|
43
|
+
return_code.to_s.include?('ssl') ? TlsError : NetworkError
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
3
47
|
def initialize(response, message = nil)
|
|
4
48
|
@response = response.blank? ? Typhoeus::Response.new : response
|
|
5
49
|
@id = SecureRandom.uuid
|
|
@@ -22,9 +66,9 @@ module NxtHttpClient
|
|
|
22
66
|
id: id,
|
|
23
67
|
url: url,
|
|
24
68
|
response_code: response_code,
|
|
25
|
-
request_options: request_options,
|
|
69
|
+
request_options: redact_credentials(request_options),
|
|
26
70
|
response_headers: response_headers,
|
|
27
|
-
request_headers: request_headers,
|
|
71
|
+
request_headers: redact_authorization(request_headers),
|
|
28
72
|
body: body,
|
|
29
73
|
x_request_id: x_request_id
|
|
30
74
|
}
|
|
@@ -77,5 +121,46 @@ module NxtHttpClient
|
|
|
77
121
|
def response_content_type
|
|
78
122
|
response_headers['Content-Type']
|
|
79
123
|
end
|
|
124
|
+
|
|
125
|
+
private
|
|
126
|
+
|
|
127
|
+
# Keep Authorization tokens / basic-auth creds out of serialized output (to_h reaches Sentry).
|
|
128
|
+
def redact_credentials(options)
|
|
129
|
+
options = options.merge('userpwd' => REDACTED) if options.key?('userpwd')
|
|
130
|
+
return options unless options['headers'].respond_to?(:key?)
|
|
131
|
+
|
|
132
|
+
options.merge('headers' => redact_authorization(options['headers']))
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def redact_authorization(headers)
|
|
136
|
+
return headers unless headers.respond_to?(:keys)
|
|
137
|
+
|
|
138
|
+
# HTTP header names are case-insensitive, and HashWithIndifferentAccess does not normalize case.
|
|
139
|
+
sensitive = headers.keys.select { |key| SENSITIVE_HEADERS.any? { |name| key.to_s.casecmp?(name) } }
|
|
140
|
+
return headers if sensitive.empty?
|
|
141
|
+
|
|
142
|
+
headers.merge(sensitive.index_with { REDACTED })
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
public
|
|
146
|
+
|
|
147
|
+
class ClientError < self; end
|
|
148
|
+
class BadRequest < ClientError; end # 400
|
|
149
|
+
class Unauthorized < ClientError; end # 401
|
|
150
|
+
class Forbidden < ClientError; end # 403
|
|
151
|
+
class NotFound < ClientError; end # 404
|
|
152
|
+
class UnprocessableEntity < ClientError; end # 422
|
|
153
|
+
class TooManyRequests < ClientError; end # 429
|
|
154
|
+
|
|
155
|
+
class ServerError < self; end
|
|
156
|
+
|
|
157
|
+
class NetworkError < self; end # code 0 (no HTTP response)
|
|
158
|
+
class Timeout < NetworkError; end # :operation_timedout
|
|
159
|
+
class ConnectionFailed < NetworkError; end # :couldnt_connect
|
|
160
|
+
class NameResolutionError < NetworkError; end # :couldnt_resolve_host / :couldnt_resolve_proxy
|
|
161
|
+
class TlsError < NetworkError; end # :ssl_connect_error and other non-cert :ssl_*
|
|
162
|
+
|
|
163
|
+
# Sibling of NetworkError, not a child, so it's excluded from `retry_on NetworkError`.
|
|
164
|
+
class CertificateError < self; end
|
|
80
165
|
end
|
|
81
166
|
end
|
metadata
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: nxt_http_client
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andreas Robecke
|
|
8
8
|
- Nils Sommer
|
|
9
9
|
- Raphael Kallensee
|
|
10
10
|
- Luetfi Demirci
|
|
11
|
-
autorequire:
|
|
12
11
|
bindir: exe
|
|
13
12
|
cert_chain: []
|
|
14
|
-
date:
|
|
13
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
15
14
|
dependencies:
|
|
16
15
|
- !ruby/object:Gem::Dependency
|
|
17
16
|
name: typhoeus
|
|
@@ -202,11 +201,10 @@ executables: []
|
|
|
202
201
|
extensions: []
|
|
203
202
|
extra_rdoc_files: []
|
|
204
203
|
files:
|
|
205
|
-
- ".
|
|
204
|
+
- ".github/workflows/ci.yml"
|
|
206
205
|
- ".gitignore"
|
|
207
206
|
- ".rspec"
|
|
208
207
|
- ".ruby-version"
|
|
209
|
-
- ".travis.yml"
|
|
210
208
|
- CHANGELOG.md
|
|
211
209
|
- Gemfile
|
|
212
210
|
- Gemfile.lock
|
|
@@ -234,7 +232,6 @@ homepage: https://github.com/nxt-insurance/nxt_http_client
|
|
|
234
232
|
licenses:
|
|
235
233
|
- MIT
|
|
236
234
|
metadata: {}
|
|
237
|
-
post_install_message:
|
|
238
235
|
rdoc_options: []
|
|
239
236
|
require_paths:
|
|
240
237
|
- lib
|
|
@@ -249,8 +246,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
249
246
|
- !ruby/object:Gem::Version
|
|
250
247
|
version: '0'
|
|
251
248
|
requirements: []
|
|
252
|
-
rubygems_version:
|
|
253
|
-
signing_key:
|
|
249
|
+
rubygems_version: 4.0.10
|
|
254
250
|
specification_version: 4
|
|
255
251
|
summary: NxtHttpClient is a simple DSL on top the typhoeus http gem
|
|
256
252
|
test_files: []
|
data/.circleci/config.yml
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
version: 2.1
|
|
2
|
-
|
|
3
|
-
orbs:
|
|
4
|
-
ruby: circleci/ruby@2.0.1
|
|
5
|
-
|
|
6
|
-
jobs:
|
|
7
|
-
build:
|
|
8
|
-
docker:
|
|
9
|
-
- image: cimg/ruby:3.3.2-node
|
|
10
|
-
- image: cimg/redis:6.0.16
|
|
11
|
-
|
|
12
|
-
working_directory: ~/repo
|
|
13
|
-
|
|
14
|
-
steps:
|
|
15
|
-
- checkout
|
|
16
|
-
|
|
17
|
-
- ruby/install-deps:
|
|
18
|
-
key: gems-v2
|
|
19
|
-
include-branch-in-cache-key: false
|
|
20
|
-
- ruby/rspec-test
|