chimera_http_client 1.7.1 → 1.9.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/action-ci.yml +58 -15
- data/.rubocop.yml +5 -0
- data/.ruby-version +1 -1
- data/README.markdown +134 -44
- data/TODO.markdown +17 -13
- data/chimera_http_client.gemspec +3 -3
- data/lib/chimera_http_client/base.rb +22 -2
- data/lib/chimera_http_client/queue.rb +1 -0
- data/lib/chimera_http_client/request.rb +46 -1
- data/lib/chimera_http_client/serializer.rb +23 -0
- data/lib/chimera_http_client/version.rb +1 -1
- metadata +9 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ca5968573054108171952f955ef7974120a4d4725ed1ca34af29e3d40e831c61
|
|
4
|
+
data.tar.gz: 3f8adfc58275d048326c4cb25c93cc46d637195da9eb042e1c6388d705e3dc54
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b00e1318e81f48ea6897e384062ab4e30b9ca83d1dc4549a22ecf8df9ce1940806c47fd04f8bf2f1109cd58922bee78605622fedc61bc74e5fb15d4be7dc0184
|
|
7
|
+
data.tar.gz: 494531a305d3bb571e98d9da1f6ad69330b64de19d29ed8d4cd64a3218fdc92bc3c2c499f7b88b829f714abc5f291654b09d364dff7577e83d6ff3080b68c6ae
|
|
@@ -1,25 +1,68 @@
|
|
|
1
|
-
name:
|
|
1
|
+
name: Ruby CI
|
|
2
|
+
|
|
2
3
|
on:
|
|
3
|
-
pull_request:
|
|
4
4
|
push:
|
|
5
|
-
branches: [
|
|
5
|
+
branches: [ master ]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ master ]
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
|
|
10
|
+
concurrency:
|
|
11
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
12
|
+
cancel-in-progress: true
|
|
13
|
+
|
|
14
|
+
permissions:
|
|
15
|
+
contents: read
|
|
16
|
+
|
|
6
17
|
jobs:
|
|
7
18
|
test:
|
|
19
|
+
name: Test on ${{ matrix.os }} / ${{ matrix.ruby }}
|
|
8
20
|
strategy:
|
|
9
21
|
fail-fast: false
|
|
10
22
|
matrix:
|
|
11
|
-
os: [ubuntu-latest]
|
|
12
|
-
ruby: ['3.3', '3.4', '
|
|
13
|
-
|
|
23
|
+
os: [ubuntu-latest]
|
|
24
|
+
ruby: ['3.3', '3.4', '4.0']
|
|
25
|
+
experimental: [false]
|
|
26
|
+
# add certain optional combos whose failures won't block merges:
|
|
27
|
+
include:
|
|
28
|
+
- os: macos-latest
|
|
29
|
+
ruby: '4.0'
|
|
30
|
+
experimental: true
|
|
31
|
+
- os: ubuntu-latest
|
|
32
|
+
ruby: 'truffleruby'
|
|
33
|
+
experimental: true
|
|
34
|
+
- os: ubuntu-latest
|
|
35
|
+
ruby: 'jruby'
|
|
36
|
+
experimental: true
|
|
37
|
+
- os: windows-latest
|
|
38
|
+
ruby: '4.0'
|
|
39
|
+
experimental: true
|
|
14
40
|
|
|
15
|
-
runs-on: ${{ matrix.os }}
|
|
41
|
+
runs-on: ${{ matrix.os }}
|
|
16
42
|
|
|
17
43
|
steps:
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
44
|
+
- uses: actions/checkout@v7
|
|
45
|
+
with:
|
|
46
|
+
fetch-depth: 0
|
|
47
|
+
|
|
48
|
+
- name: Set up Java for JRuby
|
|
49
|
+
if: ${{ matrix.ruby == 'jruby' }}
|
|
50
|
+
uses: actions/setup-java@v4
|
|
51
|
+
with:
|
|
52
|
+
distribution: temurin
|
|
53
|
+
java-version: '25'
|
|
54
|
+
|
|
55
|
+
- uses: ruby/setup-ruby@v1
|
|
56
|
+
with:
|
|
57
|
+
ruby-version: ${{ matrix.ruby }}
|
|
58
|
+
bundler-cache: true
|
|
59
|
+
|
|
60
|
+
- name: Print versions
|
|
61
|
+
run: |
|
|
62
|
+
ruby -v
|
|
63
|
+
gem -v
|
|
64
|
+
bundle -v
|
|
65
|
+
|
|
66
|
+
- name: Run linters and test suite
|
|
67
|
+
continue-on-error: ${{ matrix.experimental }}
|
|
68
|
+
run: bundle exec rake ci
|
data/.rubocop.yml
CHANGED
data/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
4.0.6
|
data/README.markdown
CHANGED
|
@@ -17,57 +17,65 @@ The only other runtime dependency is Ruby's latest code loader [**zeitwerk**](ht
|
|
|
17
17
|
^
|
|
18
18
|
### Ruby version
|
|
19
19
|
|
|
20
|
-
| Chimera version | MRI Ruby version
|
|
21
|
-
|
|
22
|
-
| >= 1.
|
|
23
|
-
| >= 1.
|
|
24
|
-
| >= 1.
|
|
25
|
-
| >= 1.
|
|
26
|
-
|
|
|
27
|
-
|
|
|
20
|
+
| Chimera version | MRI Ruby version | JRuby | TruffleRuby |
|
|
21
|
+
|:----------------|:---------------------------------------------------------------------|:-----:|:-----------:|
|
|
22
|
+
| >= 1.8 | >= 3.3 (4.0 supported, older versions untested, likely still work) | yes | yes |
|
|
23
|
+
| >= 1.7 | >= 3.3 (older versions untested, likely still work) | yes | yes |
|
|
24
|
+
| >= 1.6 | >= 2.7 (all 3.x versions supported) | yes | yes |
|
|
25
|
+
| >= 1.4 | >= 2.5 (3.0 compatibility ensured) | yes | no |
|
|
26
|
+
| >= 1.1 | >= 2.5 | ? | ? |
|
|
27
|
+
| = 1.0 | >= 2.4, <= 3.0 | ? | ? |
|
|
28
|
+
| <= 0.5 | >= 2.1, <= 3.0 | ? | ? |
|
|
28
29
|
|
|
29
30
|
The test suite of v1.4 passes on **MRI Ruby** (2.5, 2.6, 2.7, 3.0) and on **JRuby**, but not on **TruffleRuby**.
|
|
30
31
|
The test suite of v1.6 passes on **MRI Ruby** (2.7, 3.0, 3.1, 3.2, 3.3) and on **JRuby** and **TruffleRuby**.
|
|
31
|
-
|
|
32
|
+
v1.8 is no longer tested against MRI Rubies older than 3.3 (but they are likely still supported).
|
|
33
|
+
All information above is given for **Linux**.
|
|
34
|
+
**MacOS & Windows** were only tested successfully against **MRI Ruby 4.0** (older versions likely work as well).
|
|
32
35
|
|
|
33
36
|
### ENV variables
|
|
34
37
|
|
|
35
38
|
Setting the environment variable `ENV['CHIMERA_HTTP_CLIENT_LOG_REQUESTS']` to `true` (or `'true'`) will provide more detailed error messages for logging and also add additional information to the Error JSON. It is recommended to use this only in development environments.
|
|
36
39
|
|
|
40
|
+
Setting `ENV['CHIMERA_HTTP_CLIENT_DEFAULT_HEADERS']` to a JSON object string will merge those headers into every `Connection`/`Queue` in the process, e.g. `export CHIMERA_HTTP_CLIENT_DEFAULT_HEADERS='{"X-Service-Name":"orders-api"}'`. This is meant for deployment/ops-level defaults (like identifying which service is calling), set once outside application code. See [Custom headers](#custom-headers) below.
|
|
41
|
+
|
|
37
42
|
## Table of Contents
|
|
38
43
|
|
|
39
44
|
<!-- TOC depthFrom:1 depthTo:4 withLinks:1 updateOnSave:0 orderedList:0 -->
|
|
40
45
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
46
|
+
- [ChimeraHttpClient](#chimerahttpclient)
|
|
47
|
+
- [Dependencies](#dependencies)
|
|
48
|
+
- [Ruby version](#ruby-version)
|
|
49
|
+
- [ENV variables](#env-variables)
|
|
50
|
+
- [Table of Contents](#table-of-contents)
|
|
51
|
+
- [The Connection class](#the-connection-class)
|
|
52
|
+
- [Initialization](#initialization)
|
|
53
|
+
- [Mandatory initialization parameter `base_url`](#mandatory-initialization-parameter-base_url)
|
|
54
|
+
- [Optional initialization parameters](#optional-initialization-parameters)
|
|
55
|
+
- [Custom deserializers](#custom-deserializers)
|
|
56
|
+
- [Custom headers](#custom-headers)
|
|
57
|
+
- [Custom serializer](#custom-serializer)
|
|
58
|
+
- [Monitoring, metrics, instrumentation](#monitoring-metrics-instrumentation)
|
|
59
|
+
- [Request methods](#request-methods)
|
|
60
|
+
- [Mandatory request parameter `endpoint`](#mandatory-request-parameter-endpoint)
|
|
61
|
+
- [Optional request parameters](#optional-request-parameters)
|
|
62
|
+
- [Basic auth](#basic-auth)
|
|
63
|
+
- [Timeout duration](#timeout-duration)
|
|
64
|
+
- [Retrying requests](#retrying-requests)
|
|
65
|
+
- [Custom logger](#custom-logger)
|
|
66
|
+
- [Caching responses](#caching-responses)
|
|
67
|
+
- [Example usage](#example-usage)
|
|
68
|
+
- [The Request class](#the-request-class)
|
|
69
|
+
- [The Response class](#the-response-class)
|
|
70
|
+
- [Error classes](#error-classes)
|
|
71
|
+
- [The Queue class](#the-queue-class)
|
|
72
|
+
- [Queueing requests](#queueing-requests)
|
|
73
|
+
- [Executing requests in parallel](#executing-requests-in-parallel)
|
|
74
|
+
- [Empty the queue](#empty-the-queue)
|
|
75
|
+
- [Installation](#installation)
|
|
76
|
+
- [Maintainers and Contributors](#maintainers-and-contributors)
|
|
77
|
+
- [Roadmap](#roadmap)
|
|
78
|
+
- [Chimera](#chimera)
|
|
71
79
|
|
|
72
80
|
<!-- /TOC -->
|
|
73
81
|
|
|
@@ -96,8 +104,12 @@ The optional parameters are:
|
|
|
96
104
|
|
|
97
105
|
* `cache` - an instance of your cache solution, can be overwritten in any request
|
|
98
106
|
* `deserializers` - custom methods to deserialize the response body, below more details
|
|
107
|
+
* `headers` - override/extend the default request headers (`{ "Content-Type" => "application/json" }`), can be overwritten or merged with per-request headers, below more details
|
|
99
108
|
* `logger` - an instance of a logger class that implements `#info`, `#warn` and `#error` methods
|
|
100
109
|
* `monitor` - to collect metrics about requests, the basis for your instrumentation needs
|
|
110
|
+
* `retries` - the number of times a failed idempotent request is retried, can be overwritten in any request, the default is `0` (no retries)
|
|
111
|
+
* `retry_delay` - the base delay in seconds between retries, can be overwritten in any request, the default is `1`
|
|
112
|
+
* `serializer` - override how a Hash/Array request body is turned into a request, can be overwritten in any request, below more details
|
|
101
113
|
* `timeout` - the timeout for all requests, can be overwritten in any request, the default are 3 seconds
|
|
102
114
|
* `user_agent` - if you would like your calls to identify with a specific user agent
|
|
103
115
|
* `verbose` - the default is `false`, set it to true while debugging issues
|
|
@@ -112,7 +124,64 @@ A Deserializer has to be an object on which the method `call` with the parameter
|
|
|
112
124
|
|
|
113
125
|
custom_deserializer.call(body)
|
|
114
126
|
|
|
115
|
-
where `body` is the response body (in the default case a JSON object). The class `Deserializer` contains the default objects that are used. They might help you creating your own.
|
|
127
|
+
where `body` is the response body (in the default case a JSON object). The class `Deserializer` contains the default objects that are used. They might help you creating your own. If the API you connect to does not support JSON, set `headers` (see [Custom headers](#custom-headers) below) once on the `Connection` instead of repeating it on every request - and see [Custom serializer](#custom-serializer) below for the equivalent on the request-body side.
|
|
128
|
+
|
|
129
|
+
##### Custom headers
|
|
130
|
+
|
|
131
|
+
Every request sends `{ "Content-Type" => "application/json" }` plus a `User-Agent` by default. Four layers apply on top of each other (each one merges in, overriding only the keys it sets, so you never have to restate headers you're not changing):
|
|
132
|
+
|
|
133
|
+
1. the built-in default above (also available as `ChimeraHttpClient::Base::DEFAULT_HEADERS`)
|
|
134
|
+
2. `ENV['CHIMERA_HTTP_CLIENT_DEFAULT_HEADERS']`, a JSON object (see [ENV variables](#env-variables) above)
|
|
135
|
+
3. `headers` passed to `Connection.new`/`Queue.new` - the connection's own default
|
|
136
|
+
4. `headers` passed to an individual request - already documented above, unchanged
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
# connecting to a non-JSON API: override just Content-Type, everything else (User-Agent, ...) still applies
|
|
140
|
+
connection = ChimeraHttpClient::Connection.new(base_url: 'http://localhost:3000/v1', headers: { "Content-Type" => "application/xml" })
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
A common use for the connection-level `headers` option is a request/correlation id that should be attached to every call made for one unit of work (a background job, one inbound request being served), without every call site needing to know about it:
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
connection = ChimeraHttpClient::Connection.new(base_url: 'http://localhost:3000/v1', headers: { "X-Request-Id" => job_id })
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
For something that's the same for every request in the whole process instead - e.g. identifying which of your services is calling, in a service-to-service setting - set it once via `ENV['CHIMERA_HTTP_CLIENT_DEFAULT_HEADERS']` at the deployment level rather than in application code:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
export CHIMERA_HTTP_CLIENT_DEFAULT_HEADERS='{"X-Service-Name":"orders-api"}'
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
##### Custom serializer
|
|
156
|
+
|
|
157
|
+
A `Hash` or `Array` `body` is automatically serialized before the request is sent - by default with `body.to_json`, so this now just works:
|
|
158
|
+
|
|
159
|
+
```ruby
|
|
160
|
+
connection.post!('users', body: { name: "Andy" }) # no more body.to_json needed
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
A `String` body (e.g. one you already serialized yourself) is always passed through completely unchanged - so any existing code still doing `body: body.to_json` keeps working exactly as before.
|
|
164
|
+
|
|
165
|
+
To use a different format, pass `serializer` to `Connection.new`/`Queue.new`, or to an individual request. A Serializer has to be an object on which the method `call` with the parameter `body` can be called:
|
|
166
|
+
|
|
167
|
+
custom_serializer.call(body)
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
# talking to an XML API instead
|
|
171
|
+
connection = ChimeraHttpClient::Connection.new(base_url: 'http://localhost:3000/v1', serializer: ->(body) { body.to_xml })
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
The serializer normally returns a `String`, but it can also return the `Hash` unconverted - Typhoeus (via Ethon/libcurl) then form-encodes it natively as `application/x-www-form-urlencoded`, or as real multipart if a value looks file-shaped:
|
|
175
|
+
|
|
176
|
+
```ruby
|
|
177
|
+
# posting a plain form instead of JSON
|
|
178
|
+
connection.post(
|
|
179
|
+
'login',
|
|
180
|
+
body: { username: "andy", password: "secret" },
|
|
181
|
+
serializer: ->(body) { body }, # hand the Hash to Typhoeus/Ethon unconverted
|
|
182
|
+
headers: { "Content-Type" => "application/x-www-form-urlencoded" }
|
|
183
|
+
)
|
|
184
|
+
```
|
|
116
185
|
|
|
117
186
|
##### Monitoring, metrics, instrumentation
|
|
118
187
|
|
|
@@ -171,6 +240,9 @@ All request methods expect a mandatory `endpoint` and an optional hash as parame
|
|
|
171
240
|
* `password` - used for a BasicAuth login
|
|
172
241
|
* `timeout` - set a custom timeout per request (the default is 3 seconds)
|
|
173
242
|
* `cache` - optionally overwrite the cache store set in `Connection` in any request
|
|
243
|
+
* `retries` - optionally overwrite the number of retries set in `Connection` for this request
|
|
244
|
+
* `retry_delay` - optionally overwrite the retry delay set in `Connection` for this request
|
|
245
|
+
* `serializer` - optionally overwrite the body serializer set in `Connection` for this request
|
|
174
246
|
* `monitoring_context` - pass additional information you want to collect with your instrumentation `monitor`
|
|
175
247
|
|
|
176
248
|
Example:
|
|
@@ -197,6 +269,24 @@ The default timeout duration is **3 seconds**.
|
|
|
197
269
|
|
|
198
270
|
If you want to use a different timeout, you can pass the key `timeout` when initializing the `Connection`. You can also overwrite it on every call.
|
|
199
271
|
|
|
272
|
+
#### Retrying requests
|
|
273
|
+
|
|
274
|
+
Pass `retries` (and optionally `retry_delay`) to `Connection.new`/`Queue.new`, or to any individual request, to automatically retry on transient failures:
|
|
275
|
+
|
|
276
|
+
```ruby
|
|
277
|
+
connection = ChimeraHttpClient::Connection.new(base_url: 'http://localhost:3000/v1', retries: 3, retry_delay: 1)
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
* `retries` - the maximum number of retry attempts. The default is `0` (no retries, fully opt-in).
|
|
281
|
+
* `retry_delay` - the base delay in seconds before the first retry. The default is `1`. Each subsequent retry doubles the previous delay (a fixed 2x backoff, not configurable): with `retry_delay: 1` the delays are `1s, 2s, 4s, ...`.
|
|
282
|
+
|
|
283
|
+
Retries are automatic and safe by design - they only apply to:
|
|
284
|
+
|
|
285
|
+
* **idempotent methods**: `get`, `put`, `delete`, `head` - never `post`/`patch`, regardless of the configured `retries`, since retrying a non-idempotent write could duplicate side effects.
|
|
286
|
+
* **transient errors**: `ConnectionError`, `TimeoutError`, `ServerError` (5xx) - never 4xx `ClientError`s, since retrying those can't change the outcome.
|
|
287
|
+
|
|
288
|
+
> Note for `Queue`: retries are implemented by re-queueing the failed request onto the same `Typhoeus::Hydra` that's already running the batch, so a retry can start as soon as its own request fails rather than waiting for the whole batch. One consequence: the `retry_delay` sleep happens inside that request's completion callback, which briefly pauses progress on *every other* in-flight request in the same queue (libcurl's multi interface is a single-threaded, cooperative event loop). This doesn't affect `Connection`, whose retries run sequentially with nothing else in flight.
|
|
289
|
+
|
|
200
290
|
#### Custom logger
|
|
201
291
|
|
|
202
292
|
By default no logging is happening. If you need request logging, you can pass your custom Logger to the key `logger` when initializing the `Connection`. It will write to `logger.info` when starting and when completing a request.
|
|
@@ -287,7 +377,7 @@ class Users
|
|
|
287
377
|
# CREATE a new user by sending attributes in a JSON body and instantiate the new User
|
|
288
378
|
#
|
|
289
379
|
def create(body:)
|
|
290
|
-
response = connection.post!('users', body: body
|
|
380
|
+
response = connection.post!('users', body: body) # a Hash body is serialized to JSON automatically
|
|
291
381
|
|
|
292
382
|
user = response.parsed_body
|
|
293
383
|
User.new(id: user['id'], name: user['name'], email: user['email'])
|
|
@@ -322,7 +412,7 @@ To create and fetch a user from a remote service with the `Users` wrapper listed
|
|
|
322
412
|
|
|
323
413
|
Usually it does not have to be used directly. It is the class that executes the `Typhoeus::Requests`, raises `Errors` on failing and returns `Response` objects on successful calls.
|
|
324
414
|
|
|
325
|
-
|
|
415
|
+
By the time `Request` receives `body`, it's already in the (serialized) form the endpoint expects: `Connection`/`Queue` auto-serialize a `Hash`/`Array` body to JSON (or via a custom `serializer`, see [Custom serializer](#custom-serializer)) before it ever reaches `Request`. A `String` body is passed through unchanged.
|
|
326
416
|
|
|
327
417
|
## The Response class
|
|
328
418
|
|
|
@@ -434,8 +524,8 @@ After checking out the repo, run `bundle install` and then `bundle execute rake`
|
|
|
434
524
|
|
|
435
525
|
> The test suite uses a Sinatra server to make real HTTP requests. It is mounted via Capybara_discoball and running in the same process. It is still running reasonably fast (on my MacBook Air):
|
|
436
526
|
|
|
437
|
-
Finished in
|
|
438
|
-
|
|
527
|
+
Finished in 1.02 seconds (files took 0.43805 seconds to load)
|
|
528
|
+
882 examples, 0 failures, 7 pending
|
|
439
529
|
|
|
440
530
|
You can also run `rake console` to open an irb session with the `ChimeraHttpClient` pre-loaded that will allow you to experiment.
|
|
441
531
|
|
data/TODO.markdown
CHANGED
|
@@ -22,6 +22,12 @@ _none known_
|
|
|
22
22
|
* [x] ~~use custom deserializer in #parsed_body instead of default JSON parsing~~
|
|
23
23
|
* [x] ~~add example to README~~
|
|
24
24
|
|
|
25
|
+
### ~~Custom serializer~~
|
|
26
|
+
|
|
27
|
+
* [x] ~~allow to pass custom serializer~~
|
|
28
|
+
* [x] ~~use custom serializer with body instead of default JSON serialization ~~
|
|
29
|
+
* [x] ~~add example to README~~
|
|
30
|
+
|
|
25
31
|
### Queueing / running in parallel
|
|
26
32
|
|
|
27
33
|
* [x] ~~allow to queue multiple requests~~
|
|
@@ -79,12 +85,12 @@ _none known_
|
|
|
79
85
|
* [ ] enable to pass on_headers, on_body, on_complete procs
|
|
80
86
|
* [ ] add example to README
|
|
81
87
|
|
|
82
|
-
### HTTP Headers
|
|
88
|
+
### ~~HTTP Headers~~
|
|
83
89
|
|
|
84
|
-
* [
|
|
85
|
-
* [
|
|
86
|
-
* [
|
|
87
|
-
* [
|
|
90
|
+
* [x] ~~make Connection#default_headers configurable~~
|
|
91
|
+
* [x] ~~allow to set default_headers via ENV vars~~
|
|
92
|
+
* [x] ~~give example how to use default_headers (e.g. request_id)~~
|
|
93
|
+
* [x] ~~explain in README how to benefit from this gem in a given setting~~
|
|
88
94
|
|
|
89
95
|
### ~~Caching~~
|
|
90
96
|
|
|
@@ -105,15 +111,13 @@ _none known_
|
|
|
105
111
|
* [x] ~~hook up Travis-CI~~
|
|
106
112
|
* [x] ~~ensure it runs with Ruby 2.4 and newer~~
|
|
107
113
|
|
|
108
|
-
### Retry Requests
|
|
114
|
+
### ~~Retry Requests~~
|
|
109
115
|
|
|
110
|
-
* [
|
|
111
|
-
* [
|
|
112
|
-
* [
|
|
113
|
-
* [
|
|
114
|
-
* [
|
|
115
|
-
* [ ] or document in README how to build a retry mechanism
|
|
116
|
-
* [ ] https://gist.github.com/kunalmodi/2939288
|
|
116
|
+
* [x] ~~leverage Hydra to retry failed calls (Queue); Connection uses a sequential retry loop instead~~
|
|
117
|
+
* [x] ~~configure number of retries~~
|
|
118
|
+
* [x] ~~configure delay between retries~~
|
|
119
|
+
* [x] ~~retry idempotent calls GET, PUT, DELETE, HEAD automatically~~
|
|
120
|
+
* [x] ~~add example to README~~
|
|
117
121
|
|
|
118
122
|
### Queueing / run requests serialized
|
|
119
123
|
|
data/chimera_http_client.gemspec
CHANGED
|
@@ -40,9 +40,9 @@ Gem::Specification.new do |spec|
|
|
|
40
40
|
spec.add_development_dependency "irb", ">= 1.15"
|
|
41
41
|
spec.add_development_dependency "rake", ">= 13.2"
|
|
42
42
|
spec.add_development_dependency "rspec", "~> 3.13"
|
|
43
|
-
spec.add_development_dependency "rubocop", "~> 1.75
|
|
44
|
-
spec.add_development_dependency "rubocop-rake", "~> 0.7
|
|
45
|
-
spec.add_development_dependency "rubocop-rspec", "~> 3.
|
|
43
|
+
spec.add_development_dependency "rubocop", "~> 1.75"
|
|
44
|
+
spec.add_development_dependency "rubocop-rake", "~> 0.7"
|
|
45
|
+
spec.add_development_dependency "rubocop-rspec", "~> 3.10"
|
|
46
46
|
|
|
47
47
|
# only for server specs with real HTTP requests
|
|
48
48
|
spec.add_development_dependency "capybara", "~> 3.40"
|
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
module ChimeraHttpClient
|
|
2
2
|
class Base
|
|
3
3
|
USER_AGENT = "ChimeraHttpClient (by mediafinger)".freeze
|
|
4
|
+
DEFAULT_HEADERS = { "Content-Type" => "application/json" }.freeze
|
|
5
|
+
DEFAULT_HEADERS_ENV_VAR = "CHIMERA_HTTP_CLIENT_DEFAULT_HEADERS".freeze
|
|
4
6
|
|
|
5
7
|
def initialize(options = {})
|
|
6
8
|
fail(ChimeraHttpClient::ParameterMissingError, "base_url expected, but not given") if options[:base_url].nil?
|
|
7
9
|
|
|
8
10
|
@base_url = options.fetch(:base_url)
|
|
9
11
|
@deserializer = default_deserializer.merge(options.fetch(:deserializer, {}))
|
|
12
|
+
@serializer = options.fetch(:serializer, ::ChimeraHttpClient::Serializer.json)
|
|
10
13
|
@logger = options[:logger]
|
|
11
14
|
@monitor = options[:monitor]
|
|
12
15
|
@timeout = options[:timeout]
|
|
13
16
|
@cache = options[:cache]
|
|
17
|
+
@headers = DEFAULT_HEADERS.merge(headers_from_env).merge(options[:headers] || {})
|
|
14
18
|
@user_agent = options.fetch(:user_agent, USER_AGENT)
|
|
15
19
|
@verbose = options.fetch(:verbose, false)
|
|
20
|
+
@retries = options[:retries] || 0
|
|
21
|
+
@retry_delay = options[:retry_delay] || 1
|
|
16
22
|
end
|
|
17
23
|
|
|
18
24
|
private
|
|
@@ -22,6 +28,8 @@ module ChimeraHttpClient
|
|
|
22
28
|
options[:timeout] ||= @timeout
|
|
23
29
|
options[:cache] = @cache if options[:cache].nil?
|
|
24
30
|
options[:verbose] = @verbose if options[:verbose].nil?
|
|
31
|
+
options[:retries] = @retries if options[:retries].nil?
|
|
32
|
+
options[:retry_delay] = @retry_delay if options[:retry_delay].nil?
|
|
25
33
|
|
|
26
34
|
options
|
|
27
35
|
end
|
|
@@ -29,10 +37,13 @@ module ChimeraHttpClient
|
|
|
29
37
|
def extract_body(options)
|
|
30
38
|
body = options.delete(:body)
|
|
31
39
|
body_optional = options.delete(:body_optional)
|
|
40
|
+
serializer = options.delete(:serializer) || @serializer
|
|
32
41
|
|
|
33
42
|
fail(ChimeraHttpClient::ParameterMissingError, "body expected, but not given") if body.nil? && !body_optional
|
|
34
43
|
|
|
35
|
-
body
|
|
44
|
+
return body unless body.is_a?(Hash) || body.is_a?(Array)
|
|
45
|
+
|
|
46
|
+
serializer.call(body)
|
|
36
47
|
end
|
|
37
48
|
|
|
38
49
|
def extract_headers(options, headers)
|
|
@@ -41,7 +52,16 @@ module ChimeraHttpClient
|
|
|
41
52
|
end
|
|
42
53
|
|
|
43
54
|
def default_headers
|
|
44
|
-
|
|
55
|
+
@headers.merge("User-Agent" => @user_agent)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def headers_from_env
|
|
59
|
+
raw = ENV[DEFAULT_HEADERS_ENV_VAR].to_s
|
|
60
|
+
return {} if raw.strip.empty?
|
|
61
|
+
|
|
62
|
+
JSON.parse(raw)
|
|
63
|
+
rescue JSON::ParserError => e
|
|
64
|
+
raise(ChimeraHttpClient::ParameterMissingError, "ENV['#{DEFAULT_HEADERS_ENV_VAR}'] is not valid JSON: #{e.message}")
|
|
45
65
|
end
|
|
46
66
|
|
|
47
67
|
def default_deserializer
|
|
@@ -2,10 +2,20 @@ module ChimeraHttpClient
|
|
|
2
2
|
class Request
|
|
3
3
|
TIMEOUT_SECONDS = 3
|
|
4
4
|
|
|
5
|
+
# Only idempotent methods are retried automatically, regardless of the configured retries count
|
|
6
|
+
RETRYABLE_METHODS = %i(get put delete head).freeze
|
|
7
|
+
|
|
8
|
+
# Only network failures and 5xx are retried - 4xx client errors can't be fixed by retrying
|
|
9
|
+
RETRYABLE_ERRORS = [ConnectionError, TimeoutError, ServerError].freeze
|
|
10
|
+
|
|
11
|
+
RETRY_BACKOFF_MULTIPLIER = 2
|
|
12
|
+
|
|
5
13
|
attr_reader :request, :result
|
|
6
14
|
|
|
7
15
|
def initialize(options = {})
|
|
8
16
|
@options = options
|
|
17
|
+
@hydra = options[:hydra]
|
|
18
|
+
@attempt = 0
|
|
9
19
|
end
|
|
10
20
|
|
|
11
21
|
def run(url:, method:, body: nil, options: {}, headers: {})
|
|
@@ -13,6 +23,15 @@ module ChimeraHttpClient
|
|
|
13
23
|
|
|
14
24
|
@request.run
|
|
15
25
|
|
|
26
|
+
attempt = 0
|
|
27
|
+
while retryable?(method, @result, options, attempt)
|
|
28
|
+
attempt += 1
|
|
29
|
+
sleep(delay_for(attempt, options))
|
|
30
|
+
|
|
31
|
+
create(url: url, method: method, body: body, options: options, headers: headers)
|
|
32
|
+
@request.run
|
|
33
|
+
end
|
|
34
|
+
|
|
16
35
|
@result
|
|
17
36
|
end
|
|
18
37
|
|
|
@@ -57,7 +76,21 @@ module ChimeraHttpClient
|
|
|
57
76
|
}
|
|
58
77
|
)
|
|
59
78
|
|
|
60
|
-
|
|
79
|
+
result = on_complete_handler(response)
|
|
80
|
+
|
|
81
|
+
# Used for queued requests (Queue): retries are re-queued onto the same Hydra from
|
|
82
|
+
# inside this callback, per Typhoeus::Hydra::Queueable#queue ("can even be done while
|
|
83
|
+
# the hydra is running"). Connection-built requests never have a @hydra, so this branch
|
|
84
|
+
# never triggers for them - their retries are handled by the loop in #run instead.
|
|
85
|
+
if @hydra && retryable?(method, result, options, @attempt)
|
|
86
|
+
@attempt += 1
|
|
87
|
+
sleep(delay_for(@attempt, options))
|
|
88
|
+
|
|
89
|
+
create(url: url, method: method, body: body, options: options, headers: headers)
|
|
90
|
+
@hydra.queue(@request)
|
|
91
|
+
else
|
|
92
|
+
@result = result
|
|
93
|
+
end
|
|
61
94
|
end
|
|
62
95
|
|
|
63
96
|
@options[:logger]&.info(
|
|
@@ -76,6 +109,18 @@ module ChimeraHttpClient
|
|
|
76
109
|
|
|
77
110
|
private
|
|
78
111
|
|
|
112
|
+
def retryable?(method, result, options, attempt)
|
|
113
|
+
result.error? &&
|
|
114
|
+
RETRYABLE_METHODS.include?(method) &&
|
|
115
|
+
RETRYABLE_ERRORS.any? { |klass| result.is_a?(klass) } &&
|
|
116
|
+
attempt < options[:retries].to_i
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Delay before retry attempt n (1-indexed): retry_delay * RETRY_BACKOFF_MULTIPLIER**(n - 1)
|
|
120
|
+
def delay_for(attempt, options)
|
|
121
|
+
options[:retry_delay].to_f * (RETRY_BACKOFF_MULTIPLIER**(attempt - 1))
|
|
122
|
+
end
|
|
123
|
+
|
|
79
124
|
def on_complete_handler(response)
|
|
80
125
|
return Response.new(response, @options) if response.success?
|
|
81
126
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# The default JSON serializer for request bodies.
|
|
2
|
+
#
|
|
3
|
+
# To use a custom serializer, pass it as a param to Connection.new or Queue.new:
|
|
4
|
+
# `serializer: your_serializer`
|
|
5
|
+
# or override it for a single request: `connection.post(endpoint, body: {...}, serializer: your_serializer)`
|
|
6
|
+
#
|
|
7
|
+
# A Serializer has to be an object on which the method `call` with the parameter `body` can be called:
|
|
8
|
+
# `custom_serializer.call(body)`
|
|
9
|
+
# It is only ever invoked for a Hash or Array body - anything else (e.g. an already-serialized
|
|
10
|
+
# String) is passed to the request unchanged. It normally returns a String, but may also return
|
|
11
|
+
# a Hash - Typhoeus/Ethon will then form-encode it natively (application/x-www-form-urlencoded,
|
|
12
|
+
# or multipart if a value looks file-shaped), so an identity serializer (`->(body) { body }`) is
|
|
13
|
+
# a valid way to opt into that instead of JSON.
|
|
14
|
+
#
|
|
15
|
+
module ChimeraHttpClient
|
|
16
|
+
class Serializer
|
|
17
|
+
class << self
|
|
18
|
+
def json
|
|
19
|
+
proc { |body| body.to_json }
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: chimera_http_client
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andreas Finger
|
|
@@ -127,42 +127,42 @@ dependencies:
|
|
|
127
127
|
requirements:
|
|
128
128
|
- - "~>"
|
|
129
129
|
- !ruby/object:Gem::Version
|
|
130
|
-
version: 1.75
|
|
130
|
+
version: '1.75'
|
|
131
131
|
type: :development
|
|
132
132
|
prerelease: false
|
|
133
133
|
version_requirements: !ruby/object:Gem::Requirement
|
|
134
134
|
requirements:
|
|
135
135
|
- - "~>"
|
|
136
136
|
- !ruby/object:Gem::Version
|
|
137
|
-
version: 1.75
|
|
137
|
+
version: '1.75'
|
|
138
138
|
- !ruby/object:Gem::Dependency
|
|
139
139
|
name: rubocop-rake
|
|
140
140
|
requirement: !ruby/object:Gem::Requirement
|
|
141
141
|
requirements:
|
|
142
142
|
- - "~>"
|
|
143
143
|
- !ruby/object:Gem::Version
|
|
144
|
-
version: 0.7
|
|
144
|
+
version: '0.7'
|
|
145
145
|
type: :development
|
|
146
146
|
prerelease: false
|
|
147
147
|
version_requirements: !ruby/object:Gem::Requirement
|
|
148
148
|
requirements:
|
|
149
149
|
- - "~>"
|
|
150
150
|
- !ruby/object:Gem::Version
|
|
151
|
-
version: 0.7
|
|
151
|
+
version: '0.7'
|
|
152
152
|
- !ruby/object:Gem::Dependency
|
|
153
153
|
name: rubocop-rspec
|
|
154
154
|
requirement: !ruby/object:Gem::Requirement
|
|
155
155
|
requirements:
|
|
156
156
|
- - "~>"
|
|
157
157
|
- !ruby/object:Gem::Version
|
|
158
|
-
version: 3.
|
|
158
|
+
version: '3.10'
|
|
159
159
|
type: :development
|
|
160
160
|
prerelease: false
|
|
161
161
|
version_requirements: !ruby/object:Gem::Requirement
|
|
162
162
|
requirements:
|
|
163
163
|
- - "~>"
|
|
164
164
|
- !ruby/object:Gem::Version
|
|
165
|
-
version: 3.
|
|
165
|
+
version: '3.10'
|
|
166
166
|
- !ruby/object:Gem::Dependency
|
|
167
167
|
name: capybara
|
|
168
168
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -292,6 +292,7 @@ files:
|
|
|
292
292
|
- lib/chimera_http_client/queue.rb
|
|
293
293
|
- lib/chimera_http_client/request.rb
|
|
294
294
|
- lib/chimera_http_client/response.rb
|
|
295
|
+
- lib/chimera_http_client/serializer.rb
|
|
295
296
|
- lib/chimera_http_client/version.rb
|
|
296
297
|
homepage: https://github.com/mediafinger/chimera_http_client
|
|
297
298
|
licenses:
|
|
@@ -312,7 +313,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
312
313
|
- !ruby/object:Gem::Version
|
|
313
314
|
version: '0'
|
|
314
315
|
requirements: []
|
|
315
|
-
rubygems_version:
|
|
316
|
+
rubygems_version: 4.0.16
|
|
316
317
|
specification_version: 4
|
|
317
318
|
summary: General http client functionality to quickly connect to JSON REST API endpoints
|
|
318
319
|
and any others
|