fulfil_api 0.6.3 → 0.7.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 +6 -0
- data/README.md +52 -1
- data/lib/fulfil_api/client.rb +38 -10
- data/lib/fulfil_api/configuration.rb +36 -4
- data/lib/fulfil_api/http_error.rb +178 -0
- data/lib/fulfil_api/relation/batchable.rb +7 -11
- data/lib/fulfil_api/tpl_client.rb +39 -14
- data/lib/fulfil_api/version.rb +1 -1
- metadata +7 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2971bc621921930210bce77c057d6da8ce549cfe476fb1fc29d72f10c4447303
|
|
4
|
+
data.tar.gz: 97658bf767d8909e359ad66441de0f81108982c6383070778eb04d3386c2fb3d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 020e57b3a676a3c56f2d2b76e5b269010764d3a95f41ef19848f0477e4f77ac9db9871252c68efe77fe35af1fbd6f319d8283f43b578caf7e09e8de4651120a9
|
|
7
|
+
data.tar.gz: 51af4a10179668acf8977fca9dccfc133bbb25516c7464cb87bd05f9abf0a5926b1631620cf9265a9637b7b0ffa8c7b9a6618bf9ee7b75912005eb3f1722e1a3
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
- Raise a `FulfilApi::HttpError` instead of a generic `FulfilApi::Error` when a request to Fulfil fails, with a dedicated subclass per HTTP status code (e.g. `FulfilApi::HttpError::TooManyRequests` for a 429). The exception message is now the description reported by Fulfil rather than its serialized response body, and the status code, body and headers are available through `#status_code`, `#response_body` and `#response_headers`. `FulfilApi::HttpError` inherits from `FulfilApi::Error`, so existing rescues keep working.
|
|
4
|
+
|
|
5
|
+
- Re-enable Ruby's built-in retry for idempotent requests on the persistent connection, which the `net_http_persistent` adapter disables by forcing `max_retries` to `0`. This recovers stale keep-alive sockets transparently instead of surfacing them as read timeouts.
|
|
6
|
+
- Add a `connection_options` configuration option to tune the persistent connection (`max_retries`, `idle_timeout`, `pool_size`).
|
|
7
|
+
- `FulfilApi.with_config` now merges the temporary options over the active configuration instead of replacing it, so a block inherits credentials and other unspecified settings rather than resetting them to their defaults.
|
|
8
|
+
|
|
3
9
|
## [0.1.0] - 2024-08-10
|
|
4
10
|
|
|
5
11
|
- Initial release
|
data/README.md
CHANGED
|
@@ -41,6 +41,8 @@ end
|
|
|
41
41
|
|
|
42
42
|
#### Using a Dynamic Configuration
|
|
43
43
|
|
|
44
|
+
`with_config` temporarily applies options **on top of the currently active configuration** (per thread) and reverts when the block returns. The options you pass are merged over the active config, so you only need to specify what changes — credentials and other settings are inherited rather than reset to their defaults.
|
|
45
|
+
|
|
44
46
|
```ruby
|
|
45
47
|
FulfilApi.with_config(
|
|
46
48
|
access_token: FulfilApi::AccessToken.new(ENV["FULFIL_API_KEY"]),
|
|
@@ -50,6 +52,17 @@ FulfilApi.with_config(
|
|
|
50
52
|
end
|
|
51
53
|
```
|
|
52
54
|
|
|
55
|
+
This makes it easy to use different settings in contexts with different constraints. For example, a web request bound by a 30s timeout can keep tight defaults globally, while a background job (which has more time) overrides just the timeouts and retries without re-passing credentials:
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
FulfilApi.with_config(
|
|
59
|
+
request_options: { open_timeout: 5, read_timeout: 60, write_timeout: 30 },
|
|
60
|
+
connection_options: { max_retries: 3, idle_timeout: 10 }
|
|
61
|
+
) do
|
|
62
|
+
# Long-running work against the Fulfil API
|
|
63
|
+
end
|
|
64
|
+
```
|
|
65
|
+
|
|
53
66
|
#### Available Configuration Options
|
|
54
67
|
|
|
55
68
|
The following configuration options are (currently) available throught both configuration methods:
|
|
@@ -60,7 +73,16 @@ The following configuration options are (currently) available throught both conf
|
|
|
60
73
|
|
|
61
74
|
- `merchant_id` (`String`): The `merchant_id` is the subdomain that the Fulfil instance is hosted on. This configuration option is required to be able to query Fulfil's API endpoints.
|
|
62
75
|
|
|
63
|
-
- `request_options` (`Hash`): The `request_options` are the
|
|
76
|
+
- `request_options` (`Hash`): The `request_options` are the per-request timeout options for the HTTP client. See [https://lostisland.github.io/faraday/#/customization/request-options](https://lostisland.github.io/faraday/#/customization/request-options) in `faraday`.
|
|
77
|
+
|
|
78
|
+
> **NOTE:** With the persistent (keep-alive) adapter there is no single whole-request `timeout`; Faraday resolves `read_timeout`, `open_timeout`, and `write_timeout` independently. `read_timeout` is the value that governs a slow or stalled response.
|
|
79
|
+
|
|
80
|
+
- `connection_options` (`Hash`): Tuning for the persistent (keep-alive) connection. Supported keys:
|
|
81
|
+
- `max_retries` (default `1`): Re-enables Ruby's built-in retry for **idempotent** requests (`GET`/`HEAD`/`PUT`/`DELETE`/`OPTIONS`). The `net_http_persistent` adapter disables this by forcing it to `0`, which makes a keep-alive socket the server has already dropped surface as a read timeout instead of being retried transparently on a fresh socket. `POST` is never auto-retried, so this is side-effect safe. Set to `0` to restore the adapter's default behaviour.
|
|
82
|
+
- `idle_timeout` (`Integer`, optional): Seconds a pooled socket may sit idle before it is recycled. Lower this towards your server's keep-alive window to shrink the stale-socket window for non-idempotent requests.
|
|
83
|
+
- `pool_size` (`Integer`, optional): Maximum number of concurrent connections kept in the pool.
|
|
84
|
+
|
|
85
|
+
> **NOTE:** When retries are enabled, the worst-case time for a request is roughly `(max_retries + 1) × read_timeout`. On platforms with a hard request cap (e.g. Heroku's 30s router limit), keep `read_timeout` low enough that this product stays under the cap.
|
|
64
86
|
|
|
65
87
|
### Querying the Fulfil API
|
|
66
88
|
|
|
@@ -127,6 +149,35 @@ line_items = FulfilApi::Resource.set(model_name: "sale.line").where(["id", "in",
|
|
|
127
149
|
line_items = FulfilApi::Resource.set(model_name: "sale.line").find_by(["sale.id", "=", 100])
|
|
128
150
|
```
|
|
129
151
|
|
|
152
|
+
### Handling Errors
|
|
153
|
+
|
|
154
|
+
Whenever a request to Fulfil fails, the gem raises a `FulfilApi::HttpError`. Every HTTP status code has its own subclass, named after the status code it represents, so you can rescue the exact failure you care about:
|
|
155
|
+
|
|
156
|
+
```ruby
|
|
157
|
+
begin
|
|
158
|
+
FulfilApi::Resource.set(model_name: "sale.sale").find_by(["id", "=", 100])
|
|
159
|
+
rescue FulfilApi::HttpError::TooManyRequests => exception
|
|
160
|
+
puts exception.message # => "This user has exceeded an allotted request count. Try again later."
|
|
161
|
+
puts exception.status_code # => 429
|
|
162
|
+
|
|
163
|
+
sleep exception.response_headers["retry-after"].to_i
|
|
164
|
+
retry
|
|
165
|
+
end
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
The message is the description reported by Fulfil. The raw response is available through `#response_body`, `#response_headers` and `#status_code`.
|
|
169
|
+
|
|
170
|
+
To catch anything that went wrong, rescue the base class instead:
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
rescue FulfilApi::HttpError => exception
|
|
174
|
+
Rails.logger.error("Fulfil responded with #{exception.status_code}: #{exception.message}")
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
A request that never reached Fulfil — a connection reset, a DNS failure, a timeout — has no status code to name and raises a `FulfilApi::HttpError` itself.
|
|
178
|
+
|
|
179
|
+
`FulfilApi::HttpError` inherits from `FulfilApi::Error`, the base class of every error in this gem, so code that already rescues `FulfilApi::Error` keeps working unchanged.
|
|
180
|
+
|
|
130
181
|
### Using the 3PL (TPL) Client
|
|
131
182
|
|
|
132
183
|
The gem also includes a client for Fulfil's [3PL Integration API](https://fulfil-3pl-integration-api.readme.io/reference/getting-started-with-your-api). This is a separate API that allows third-party logistics providers to interact with Fulfil on behalf of a merchant.
|
data/lib/fulfil_api/client.rb
CHANGED
|
@@ -109,7 +109,9 @@ module FulfilApi
|
|
|
109
109
|
# @return [Faraday::Connection]
|
|
110
110
|
def build_connection
|
|
111
111
|
Faraday.new(url: api_endpoint, request: configuration.request_options) do |connection|
|
|
112
|
-
connection.adapter
|
|
112
|
+
connection.adapter(:net_http_persistent, **adapter_options) do |http|
|
|
113
|
+
configure_persistent_connection(http)
|
|
114
|
+
end
|
|
113
115
|
|
|
114
116
|
# Configuration of the request middleware
|
|
115
117
|
connection.request :json
|
|
@@ -120,9 +122,40 @@ module FulfilApi
|
|
|
120
122
|
end
|
|
121
123
|
end
|
|
122
124
|
|
|
125
|
+
# Initialization options for the persistent adapter. Only `pool_size` is
|
|
126
|
+
# accepted here; `idle_timeout` and `max_retries` are applied to the live
|
|
127
|
+
# Net::HTTP::Persistent instance in {#configure_persistent_connection}.
|
|
128
|
+
#
|
|
129
|
+
# @return [Hash]
|
|
130
|
+
def adapter_options
|
|
131
|
+
options = {}
|
|
132
|
+
options[:pool_size] = configuration.connection_options[:pool_size] if configuration.connection_options[:pool_size]
|
|
133
|
+
options
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Tunes the underlying Net::HTTP::Persistent connection.
|
|
137
|
+
#
|
|
138
|
+
# The `net_http_persistent` adapter forces `max_retries` to 0 on every
|
|
139
|
+
# request, which disables Ruby's built-in retry for idempotent requests.
|
|
140
|
+
# Restoring it lets a stale keep-alive socket — one the server has already
|
|
141
|
+
# closed — be retried transparently on a fresh socket instead of surfacing
|
|
142
|
+
# as a read timeout. The config block runs after the adapter zeroes the
|
|
143
|
+
# value, so this takes effect.
|
|
144
|
+
#
|
|
145
|
+
# @param http [Net::HTTP::Persistent] The live persistent connection.
|
|
146
|
+
# @return [void]
|
|
147
|
+
def configure_persistent_connection(http)
|
|
148
|
+
if configuration.connection_options[:idle_timeout]
|
|
149
|
+
http.idle_timeout = configuration.connection_options[:idle_timeout]
|
|
150
|
+
end
|
|
151
|
+
return if configuration.connection_options[:max_retries].nil?
|
|
152
|
+
|
|
153
|
+
http.max_retries = configuration.connection_options[:max_retries]
|
|
154
|
+
end
|
|
155
|
+
|
|
123
156
|
# @return [Array] The cache key identifying a unique connection.
|
|
124
157
|
def connection_cache_key
|
|
125
|
-
[configuration.merchant_id, configuration.request_options]
|
|
158
|
+
[configuration.merchant_id, configuration.request_options, configuration.connection_options]
|
|
126
159
|
end
|
|
127
160
|
|
|
128
161
|
# @param relative_path [String] The relative path to the API endpoint.
|
|
@@ -134,15 +167,10 @@ module FulfilApi
|
|
|
134
167
|
|
|
135
168
|
# @param exception [Faraday::Error] Any error raised by Faraday during the execution
|
|
136
169
|
# of the HTTP request to the API endpoint.
|
|
170
|
+
# @raise [FulfilApi::HttpError] The error dedicated to the HTTP status code of the
|
|
171
|
+
# response of Fulfil.
|
|
137
172
|
def handle_request_error(exception)
|
|
138
|
-
raise FulfilApi::
|
|
139
|
-
exception.message,
|
|
140
|
-
details: {
|
|
141
|
-
response_body: exception.response_body,
|
|
142
|
-
response_headers: exception.response_headers,
|
|
143
|
-
response_status: exception.response_status
|
|
144
|
-
}
|
|
145
|
-
)
|
|
173
|
+
raise FulfilApi::HttpError.from_faraday_error(exception)
|
|
146
174
|
end
|
|
147
175
|
|
|
148
176
|
# @param method [Symbol, String] The HTTP verb for the HTTP request.
|
|
@@ -7,10 +7,23 @@ module FulfilApi
|
|
|
7
7
|
# to these settings.
|
|
8
8
|
class Configuration
|
|
9
9
|
attr_accessor :access_token, :api_version, :merchant_id, :request_options, :tpl
|
|
10
|
+
attr_reader :connection_options
|
|
10
11
|
|
|
11
12
|
DEFAULT_API_VERSION = "v2"
|
|
12
13
|
DEFAULT_REQUEST_OPTIONS = { open_timeout: 1, read_timeout: 5, write_timeout: 5, timeout: 5 }.freeze
|
|
13
14
|
|
|
15
|
+
# Tuning for the persistent (keep-alive) HTTP connection.
|
|
16
|
+
#
|
|
17
|
+
# `max_retries` re-enables Ruby's built-in retry for idempotent requests
|
|
18
|
+
# (GET/HEAD/PUT/DELETE/OPTIONS). The `net_http_persistent` adapter forces
|
|
19
|
+
# it to 0, which means a keep-alive socket the server has already dropped
|
|
20
|
+
# surfaces as a read timeout instead of being transparently retried on a
|
|
21
|
+
# fresh socket. POST is never auto-retried, so this is side-effect safe.
|
|
22
|
+
#
|
|
23
|
+
# `idle_timeout` and `pool_size` are passed through to the underlying
|
|
24
|
+
# Net::HTTP::Persistent connection when set.
|
|
25
|
+
DEFAULT_CONNECTION_OPTIONS = { max_retries: 1 }.freeze
|
|
26
|
+
|
|
14
27
|
# Initializes the configuration with optional settings.
|
|
15
28
|
#
|
|
16
29
|
# @param options [Hash, nil] An optional list of configuration options.
|
|
@@ -25,6 +38,16 @@ module FulfilApi
|
|
|
25
38
|
set_default_options
|
|
26
39
|
end
|
|
27
40
|
|
|
41
|
+
# Merges the provided connection options over the defaults so that, for
|
|
42
|
+
# example, setting only `idle_timeout` still keeps the default
|
|
43
|
+
# `max_retries`. Assigning `nil` resets to the defaults.
|
|
44
|
+
#
|
|
45
|
+
# @param options [Hash, nil] The connection options to apply.
|
|
46
|
+
# @return [void]
|
|
47
|
+
def connection_options=(options)
|
|
48
|
+
@connection_options = DEFAULT_CONNECTION_OPTIONS.merge(options || {})
|
|
49
|
+
end
|
|
50
|
+
|
|
28
51
|
private
|
|
29
52
|
|
|
30
53
|
# Sets the default options for the gem configuration.
|
|
@@ -36,6 +59,7 @@ module FulfilApi
|
|
|
36
59
|
def set_default_options
|
|
37
60
|
self.api_version = DEFAULT_API_VERSION if api_version.nil?
|
|
38
61
|
self.request_options = DEFAULT_REQUEST_OPTIONS if request_options.nil?
|
|
62
|
+
self.connection_options = nil if connection_options.nil?
|
|
39
63
|
end
|
|
40
64
|
end
|
|
41
65
|
|
|
@@ -75,15 +99,23 @@ module FulfilApi
|
|
|
75
99
|
end
|
|
76
100
|
end
|
|
77
101
|
|
|
78
|
-
# Temporarily applies the provided configuration options
|
|
79
|
-
# and then reverts
|
|
102
|
+
# Temporarily applies the provided configuration options on top of the
|
|
103
|
+
# currently active configuration, and then reverts after the block executes.
|
|
104
|
+
#
|
|
105
|
+
# The temporary options are merged over a copy of the active configuration, so
|
|
106
|
+
# a block only needs to specify what it overrides — credentials and other
|
|
107
|
+
# settings (`access_token`, `merchant_id`, ...) are inherited rather than
|
|
108
|
+
# reset to their defaults.
|
|
80
109
|
#
|
|
81
110
|
# @param temporary_options [Hash] A hash of temporary configuration options.
|
|
82
111
|
# @yield Executes the block with the temporary configuration.
|
|
83
112
|
# @return [void]
|
|
84
113
|
def self.with_config(temporary_options)
|
|
85
|
-
original_configuration = configuration
|
|
86
|
-
|
|
114
|
+
original_configuration = configuration
|
|
115
|
+
|
|
116
|
+
self.configuration = original_configuration.dup.tap do |config|
|
|
117
|
+
temporary_options.each { |key, value| config.public_send(:"#{key}=", value) }
|
|
118
|
+
end
|
|
87
119
|
|
|
88
120
|
yield
|
|
89
121
|
ensure
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FulfilApi
|
|
4
|
+
# The {FulfilApi::HttpError} is raised whenever a request to an API endpoint of
|
|
5
|
+
# Fulfil fails.
|
|
6
|
+
#
|
|
7
|
+
# Every HTTP status code Fulfil can respond with has a dedicated subclass, named
|
|
8
|
+
# after the status code it represents. A 429 response raises a
|
|
9
|
+
# {FulfilApi::HttpError::TooManyRequests}, a 422 an
|
|
10
|
+
# {FulfilApi::HttpError::UnprocessableEntity}, and so on. See {STATUS_CODES} for
|
|
11
|
+
# the full list. This lets callers rescue the exact failure they care about
|
|
12
|
+
# instead of rescuing everything and inspecting the status code themselves.
|
|
13
|
+
#
|
|
14
|
+
# Requests that never reached Fulfil — a connection reset, a DNS failure, a
|
|
15
|
+
# timeout — have no status code to name and raise a {FulfilApi::HttpError}.
|
|
16
|
+
#
|
|
17
|
+
# @example rescuing one specific HTTP status code
|
|
18
|
+
# begin
|
|
19
|
+
# FulfilApi::Resource.set(model_name: "sale.sale").find_by(["id", "=", 100])
|
|
20
|
+
# rescue FulfilApi::HttpError::TooManyRequests => exception
|
|
21
|
+
# sleep exception.response_headers["retry-after"].to_i
|
|
22
|
+
# retry
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
# @example rescuing any failed request
|
|
26
|
+
# begin
|
|
27
|
+
# FulfilApi::Resource.set(model_name: "sale.sale").find_by(["id", "=", 100])
|
|
28
|
+
# rescue FulfilApi::HttpError => exception
|
|
29
|
+
# Rails.logger.error("Fulfil responded with #{exception.status_code}: #{exception.message}")
|
|
30
|
+
# end
|
|
31
|
+
#
|
|
32
|
+
# @note {FulfilApi::HttpError} inherits from {FulfilApi::Error}, so any code that
|
|
33
|
+
# already rescues {FulfilApi::Error} keeps catching these exceptions.
|
|
34
|
+
class HttpError < Error
|
|
35
|
+
# The keys of an error response of Fulfil that can hold the human readable
|
|
36
|
+
# message, in the order they're preferred.
|
|
37
|
+
#
|
|
38
|
+
# Fulfil is not consistent in how it reports failures: HTTP level errors carry a
|
|
39
|
+
# `description`, application level errors a `message`, and a handful of
|
|
40
|
+
# endpoints only return an `error`.
|
|
41
|
+
MESSAGE_KEYS = %w[description message error].freeze
|
|
42
|
+
|
|
43
|
+
# Maps an HTTP status code onto the name of the {FulfilApi::HttpError} subclass
|
|
44
|
+
# representing it.
|
|
45
|
+
STATUS_CODES = {
|
|
46
|
+
400 => :BadRequest,
|
|
47
|
+
401 => :Unauthorized,
|
|
48
|
+
402 => :PaymentRequired,
|
|
49
|
+
403 => :Forbidden,
|
|
50
|
+
404 => :NotFound,
|
|
51
|
+
405 => :MethodNotAllowed,
|
|
52
|
+
406 => :NotAcceptable,
|
|
53
|
+
407 => :ProxyAuthenticationRequired,
|
|
54
|
+
408 => :RequestTimeout,
|
|
55
|
+
409 => :Conflict,
|
|
56
|
+
410 => :Gone,
|
|
57
|
+
411 => :LengthRequired,
|
|
58
|
+
412 => :PreconditionFailed,
|
|
59
|
+
413 => :PayloadTooLarge,
|
|
60
|
+
414 => :UriTooLong,
|
|
61
|
+
415 => :UnsupportedMediaType,
|
|
62
|
+
416 => :RangeNotSatisfiable,
|
|
63
|
+
417 => :ExpectationFailed,
|
|
64
|
+
418 => :ImATeapot,
|
|
65
|
+
421 => :MisdirectedRequest,
|
|
66
|
+
422 => :UnprocessableEntity,
|
|
67
|
+
423 => :Locked,
|
|
68
|
+
424 => :FailedDependency,
|
|
69
|
+
425 => :TooEarly,
|
|
70
|
+
426 => :UpgradeRequired,
|
|
71
|
+
428 => :PreconditionRequired,
|
|
72
|
+
429 => :TooManyRequests,
|
|
73
|
+
431 => :RequestHeaderFieldsTooLarge,
|
|
74
|
+
451 => :UnavailableForLegalReasons,
|
|
75
|
+
500 => :InternalServerError,
|
|
76
|
+
501 => :NotImplemented,
|
|
77
|
+
502 => :BadGateway,
|
|
78
|
+
503 => :ServiceUnavailable,
|
|
79
|
+
504 => :GatewayTimeout,
|
|
80
|
+
505 => :HttpVersionNotSupported,
|
|
81
|
+
506 => :VariantAlsoNegotiates,
|
|
82
|
+
507 => :InsufficientStorage,
|
|
83
|
+
508 => :LoopDetected,
|
|
84
|
+
510 => :NotExtended,
|
|
85
|
+
511 => :NetworkAuthenticationRequired
|
|
86
|
+
}.freeze
|
|
87
|
+
|
|
88
|
+
# Defines a dedicated exception class for every status code in {STATUS_CODES} and
|
|
89
|
+
# indexes them by their status code, so {.for_status_code} can look one up
|
|
90
|
+
# without going through {Module#const_get}.
|
|
91
|
+
CLASSES_BY_STATUS_CODE = STATUS_CODES.transform_values do |class_name|
|
|
92
|
+
const_set(class_name, Class.new(self))
|
|
93
|
+
end.freeze
|
|
94
|
+
|
|
95
|
+
class << self
|
|
96
|
+
# Looks up the {FulfilApi::HttpError} subclass representing the given HTTP
|
|
97
|
+
# status code.
|
|
98
|
+
#
|
|
99
|
+
# @param status_code [Integer, nil] The HTTP status code of the response.
|
|
100
|
+
# @return [Class<FulfilApi::HttpError>] The subclass for the status code, or
|
|
101
|
+
# {FulfilApi::HttpError} itself when the status code is unknown or absent.
|
|
102
|
+
def for_status_code(status_code)
|
|
103
|
+
CLASSES_BY_STATUS_CODE.fetch(status_code, HttpError)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Builds the most specific {FulfilApi::HttpError} for the given Faraday exception.
|
|
107
|
+
#
|
|
108
|
+
# @param exception [Faraday::Error] Any error raised by Faraday during the
|
|
109
|
+
# execution of the HTTP request to the API endpoint.
|
|
110
|
+
# @return [FulfilApi::HttpError]
|
|
111
|
+
def from_faraday_error(exception)
|
|
112
|
+
details = {
|
|
113
|
+
response_body: exception.response_body,
|
|
114
|
+
response_headers: exception.response_headers,
|
|
115
|
+
response_status: exception.response_status
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
for_status_code(exception.response_status).new(
|
|
119
|
+
message_from(exception.response_body) || exception.message, details: details
|
|
120
|
+
)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
# Extracts the human readable error message out of the response body of Fulfil.
|
|
126
|
+
#
|
|
127
|
+
# @param response_body [String, Hash, nil] The response body of the API endpoint.
|
|
128
|
+
# @return [String, nil] The message, or nil when the body holds none. An HTML
|
|
129
|
+
# error page and an empty body both yield nil.
|
|
130
|
+
def message_from(response_body)
|
|
131
|
+
body = parse(response_body)
|
|
132
|
+
return if body.nil?
|
|
133
|
+
|
|
134
|
+
MESSAGE_KEYS.map { |key| body[key] }.find { |message| message.is_a?(String) && message.present? }
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# @param response_body [String, Hash, nil] The response body of the API endpoint.
|
|
138
|
+
# @return [ActiveSupport::HashWithIndifferentAccess, nil]
|
|
139
|
+
def parse(response_body)
|
|
140
|
+
return response_body.with_indifferent_access if response_body.is_a?(Hash)
|
|
141
|
+
return if response_body.blank?
|
|
142
|
+
|
|
143
|
+
parsed_body = JSON.parse(response_body)
|
|
144
|
+
parsed_body.with_indifferent_access if parsed_body.is_a?(Hash)
|
|
145
|
+
rescue JSON::ParserError
|
|
146
|
+
nil
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Unlike {FulfilApi::Error}, the message is returned as-is. The name of the
|
|
151
|
+
# exception class already tells you what went wrong, so prefixing it only gets
|
|
152
|
+
# in the way of the description reported by Fulfil.
|
|
153
|
+
#
|
|
154
|
+
# @note {StandardError#message} delegates to {StandardError#to_s}, which still
|
|
155
|
+
# holds the message passed to the constructor.
|
|
156
|
+
#
|
|
157
|
+
# @return [String]
|
|
158
|
+
def message
|
|
159
|
+
to_s
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# @return [String, Hash, nil] The raw response body of the API endpoint of Fulfil.
|
|
163
|
+
def response_body
|
|
164
|
+
details&.dig(:response_body)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# @return [Hash, nil] The response headers of the API endpoint of Fulfil.
|
|
168
|
+
def response_headers
|
|
169
|
+
details&.dig(:response_headers)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# @return [Integer, nil] The HTTP status code of the response, or nil when the
|
|
173
|
+
# request never reached Fulfil.
|
|
174
|
+
def status_code
|
|
175
|
+
details&.dig(:response_status)
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
@@ -49,7 +49,7 @@ module FulfilApi
|
|
|
49
49
|
# @yield [FulfilApi::Relation] Yields FulfilApi::Relation
|
|
50
50
|
# objects to work with a batch of records.
|
|
51
51
|
# @return [FulfilApi::Relation]
|
|
52
|
-
def in_batches(of: 500, retries: :unlimited) # rubocop:disable Metrics/MethodLength
|
|
52
|
+
def in_batches(of: 500, retries: :unlimited) # rubocop:disable Metrics/MethodLength
|
|
53
53
|
current_retry = 0
|
|
54
54
|
current_offset = request_offset.presence || 0
|
|
55
55
|
batch_size = of
|
|
@@ -64,18 +64,14 @@ module FulfilApi
|
|
|
64
64
|
|
|
65
65
|
current_offset += 1
|
|
66
66
|
current_retry = 0 # Reset the retries back to the default
|
|
67
|
-
rescue FulfilApi::
|
|
68
|
-
if
|
|
69
|
-
|
|
70
|
-
raise RetryLimitExceeded, "the maximum number of #{retries} retries has been reached."
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
current_retry += 1
|
|
74
|
-
sleep 0.25
|
|
75
|
-
retry
|
|
67
|
+
rescue FulfilApi::HttpError::TooManyRequests
|
|
68
|
+
if retries != :unlimited && current_retry > retries
|
|
69
|
+
raise RetryLimitExceeded, "the maximum number of #{retries} retries has been reached."
|
|
76
70
|
end
|
|
77
71
|
|
|
78
|
-
|
|
72
|
+
current_retry += 1
|
|
73
|
+
sleep 0.25
|
|
74
|
+
retry
|
|
79
75
|
end
|
|
80
76
|
|
|
81
77
|
self
|
|
@@ -123,11 +123,10 @@ module FulfilApi
|
|
|
123
123
|
#
|
|
124
124
|
# @return [Faraday::Connection]
|
|
125
125
|
def build_connection
|
|
126
|
-
Faraday.new(
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
connection.adapter :net_http_persistent
|
|
126
|
+
Faraday.new(url: api_endpoint, request: configuration.request_options) do |connection|
|
|
127
|
+
connection.adapter(:net_http_persistent, **adapter_options) do |http|
|
|
128
|
+
configure_persistent_connection(http)
|
|
129
|
+
end
|
|
131
130
|
|
|
132
131
|
# Configuration of the request middleware
|
|
133
132
|
connection.request :json
|
|
@@ -138,9 +137,40 @@ module FulfilApi
|
|
|
138
137
|
end
|
|
139
138
|
end
|
|
140
139
|
|
|
140
|
+
# Initialization options for the persistent adapter. Only `pool_size` is
|
|
141
|
+
# accepted here; `idle_timeout` and `max_retries` are applied to the live
|
|
142
|
+
# Net::HTTP::Persistent instance in {#configure_persistent_connection}.
|
|
143
|
+
#
|
|
144
|
+
# @return [Hash]
|
|
145
|
+
def adapter_options
|
|
146
|
+
options = {}
|
|
147
|
+
options[:pool_size] = configuration.connection_options[:pool_size] if configuration.connection_options[:pool_size]
|
|
148
|
+
options
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Tunes the underlying Net::HTTP::Persistent connection.
|
|
152
|
+
#
|
|
153
|
+
# The `net_http_persistent` adapter forces `max_retries` to 0 on every
|
|
154
|
+
# request, which disables Ruby's built-in retry for idempotent requests.
|
|
155
|
+
# Restoring it lets a stale keep-alive socket — one the server has already
|
|
156
|
+
# closed — be retried transparently on a fresh socket instead of surfacing
|
|
157
|
+
# as a read timeout. The config block runs after the adapter zeroes the
|
|
158
|
+
# value, so this takes effect.
|
|
159
|
+
#
|
|
160
|
+
# @param http [Net::HTTP::Persistent] The live persistent connection.
|
|
161
|
+
# @return [void]
|
|
162
|
+
def configure_persistent_connection(http)
|
|
163
|
+
if configuration.connection_options[:idle_timeout]
|
|
164
|
+
http.idle_timeout = configuration.connection_options[:idle_timeout]
|
|
165
|
+
end
|
|
166
|
+
return if configuration.connection_options[:max_retries].nil?
|
|
167
|
+
|
|
168
|
+
http.max_retries = configuration.connection_options[:max_retries]
|
|
169
|
+
end
|
|
170
|
+
|
|
141
171
|
# @return [Array] The cache key identifying a unique connection.
|
|
142
172
|
def connection_cache_key
|
|
143
|
-
[merchant_id, api_version, configuration.request_options]
|
|
173
|
+
[merchant_id, api_version, configuration.request_options, configuration.connection_options]
|
|
144
174
|
end
|
|
145
175
|
|
|
146
176
|
# @param relative_path [String] The relative path to the API endpoint.
|
|
@@ -152,15 +182,10 @@ module FulfilApi
|
|
|
152
182
|
|
|
153
183
|
# @param exception [Faraday::Error] Any error raised by Faraday during the execution
|
|
154
184
|
# of the HTTP request to the API endpoint.
|
|
185
|
+
# @raise [FulfilApi::HttpError] The error dedicated to the HTTP status code of the
|
|
186
|
+
# response of Fulfil.
|
|
155
187
|
def handle_request_error(exception)
|
|
156
|
-
raise FulfilApi::
|
|
157
|
-
exception.message,
|
|
158
|
-
details: {
|
|
159
|
-
response_body: exception.response_body,
|
|
160
|
-
response_headers: exception.response_headers,
|
|
161
|
-
response_status: exception.response_status
|
|
162
|
-
}
|
|
163
|
-
)
|
|
188
|
+
raise FulfilApi::HttpError.from_faraday_error(exception)
|
|
164
189
|
end
|
|
165
190
|
|
|
166
191
|
# @param method [Symbol, String] The HTTP verb for the HTTP request.
|
data/lib/fulfil_api/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fulfil_api
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stefan Vermaas
|
|
8
|
+
autorequire:
|
|
8
9
|
bindir: exe
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date:
|
|
11
|
+
date: 2026-08-18 00:00:00.000000000 Z
|
|
11
12
|
dependencies:
|
|
12
13
|
- !ruby/object:Gem::Dependency
|
|
13
14
|
name: activesupport
|
|
@@ -85,6 +86,7 @@ files:
|
|
|
85
86
|
- lib/fulfil_api/configuration.rb
|
|
86
87
|
- lib/fulfil_api/customer_shipment.rb
|
|
87
88
|
- lib/fulfil_api/error.rb
|
|
89
|
+
- lib/fulfil_api/http_error.rb
|
|
88
90
|
- lib/fulfil_api/relation.rb
|
|
89
91
|
- lib/fulfil_api/relation/batchable.rb
|
|
90
92
|
- lib/fulfil_api/relation/countable.rb
|
|
@@ -111,6 +113,7 @@ metadata:
|
|
|
111
113
|
source_code_uri: https://www.github.com/codeturebv/fulfil_api
|
|
112
114
|
changelog_uri: https://www.github.com/codeturebv/fulfil_api/blob/main/CHANGELOG.md
|
|
113
115
|
rubygems_mfa_required: 'true'
|
|
116
|
+
post_install_message:
|
|
114
117
|
rdoc_options: []
|
|
115
118
|
require_paths:
|
|
116
119
|
- lib
|
|
@@ -125,7 +128,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
125
128
|
- !ruby/object:Gem::Version
|
|
126
129
|
version: '0'
|
|
127
130
|
requirements: []
|
|
128
|
-
rubygems_version:
|
|
131
|
+
rubygems_version: 3.5.11
|
|
132
|
+
signing_key:
|
|
129
133
|
specification_version: 4
|
|
130
134
|
summary: A HTTP client to interact the Fulfil.io API
|
|
131
135
|
test_files: []
|