fulfil_api 0.7.0 → 0.7.2
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 +4 -0
- data/README.md +73 -0
- data/lib/fulfil_api/client.rb +3 -8
- data/lib/fulfil_api/error/notifiable.rb +90 -0
- data/lib/fulfil_api/error.rb +38 -0
- data/lib/fulfil_api/http_error.rb +193 -0
- data/lib/fulfil_api/relation/batchable.rb +7 -11
- data/lib/fulfil_api/tpl_client.rb +3 -8
- data/lib/fulfil_api/version.rb +1 -1
- data/lib/fulfil_api.rb +1 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9a5ff8f7f8d9afe6a5130c2cf042875f2d79eabcdb53f61e7f229d03b2dc0c6e
|
|
4
|
+
data.tar.gz: 017f33c16b4562586e696724b8428f76bc27daeeaf61b0cd2d783f79a952b7e4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 387283adc42ad86ed86cea9c20f0c65728fb942b7ef7db70286a2dac2fcc544b0bf49a78feb520ff918faec916dabadf94067f71003bab2adb4087219fe63437
|
|
7
|
+
data.tar.gz: 8a69f8a11d5152825c8648babbe9535e533d394e13f8da819759eca069aab66bc8656517b35b5d4275ad62ffcabe4e022e93952e09f2780508bca8d90ba6830e
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
- Publish an `ActiveSupport::Notifications` event named `error.fulfil_api` whenever a `FulfilApi::Error` is raised, so an application can report failures of the Fulfil API to its APM without rescuing every call into the gem. Subscribe with `FulfilApi.on_error { |error| ... }`, or through `ActiveSupport::Notifications` directly to also reach the status code, response body and response headers of a `FulfilApi::HttpError`.
|
|
4
|
+
|
|
5
|
+
- 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.
|
|
6
|
+
|
|
3
7
|
- 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.
|
|
4
8
|
- Add a `connection_options` configuration option to tune the persistent connection (`max_retries`, `idle_timeout`, `pool_size`).
|
|
5
9
|
- `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.
|
data/README.md
CHANGED
|
@@ -149,6 +149,79 @@ line_items = FulfilApi::Resource.set(model_name: "sale.line").where(["id", "in",
|
|
|
149
149
|
line_items = FulfilApi::Resource.set(model_name: "sale.line").find_by(["sale.id", "=", 100])
|
|
150
150
|
```
|
|
151
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
|
+
|
|
181
|
+
### Subscribing to Errors
|
|
182
|
+
|
|
183
|
+
Rescuing tells you about the one call you wrapped. To report *every* failure of the Fulfil API — to count rate limit hits in your APM, to page on authentication failures, to log what Fulfil actually said — subscribe once instead.
|
|
184
|
+
|
|
185
|
+
Every `FulfilApi::Error` publishes an `ActiveSupport::Notifications` event named `error.fulfil_api` when it is raised. `FulfilApi.on_error` is the shorthand for listening to it:
|
|
186
|
+
|
|
187
|
+
```ruby
|
|
188
|
+
# config/initializers/fulfil_api.rb
|
|
189
|
+
|
|
190
|
+
FulfilApi.on_error do |error|
|
|
191
|
+
Appsignal.increment_counter("fulfil_api_errors", 1, error: error.class.name)
|
|
192
|
+
end
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Because every error carries its own class, you can report only the failures you care about:
|
|
196
|
+
|
|
197
|
+
```ruby
|
|
198
|
+
FulfilApi.on_error do |error|
|
|
199
|
+
next unless error.is_a?(FulfilApi::HttpError::TooManyRequests)
|
|
200
|
+
|
|
201
|
+
Appsignal.increment_counter("fulfil_api_rate_limit_exceeded")
|
|
202
|
+
end
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Subscribing through `ActiveSupport::Notifications` directly gives you the full payload, which carries the HTTP details of a `FulfilApi::HttpError` as separate keys — handy as metric dimensions:
|
|
206
|
+
|
|
207
|
+
```ruby
|
|
208
|
+
ActiveSupport::Notifications.subscribe(FulfilApi::Error::EVENT_NAME) do |event|
|
|
209
|
+
event.payload[:exception] # => ["FulfilApi::HttpError::TooManyRequests", "Try again later."]
|
|
210
|
+
event.payload[:exception_object] # => the FulfilApi::HttpError::TooManyRequests instance
|
|
211
|
+
event.payload[:status_code] # => 429
|
|
212
|
+
event.payload[:response_headers] # => { "retry-after" => "5", ... }
|
|
213
|
+
event.payload[:response_body] # => the raw response of Fulfil
|
|
214
|
+
end
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Both methods return the subscriber, so you can stop listening again with `ActiveSupport::Notifications.unsubscribe(subscriber)`.
|
|
218
|
+
|
|
219
|
+
A few things worth knowing:
|
|
220
|
+
|
|
221
|
+
- The event is published when an error is **raised**, not when it is built. Rescuing it afterwards does not suppress the notification, and retrying a request publishes one event per attempt — which is exactly what you want when counting rate limit hits.
|
|
222
|
+
- The subscriber runs on the thread that raised the error, while the error travels up the stack. Keep it cheap and hand anything slow to a background job.
|
|
223
|
+
- A subscriber cannot change the behaviour of your application. If it raises, the exception is swallowed and reported on `$stderr` rather than replacing the `FulfilApi::Error` on its way up.
|
|
224
|
+
|
|
152
225
|
### Using the 3PL (TPL) Client
|
|
153
226
|
|
|
154
227
|
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
|
@@ -167,15 +167,10 @@ module FulfilApi
|
|
|
167
167
|
|
|
168
168
|
# @param exception [Faraday::Error] Any error raised by Faraday during the execution
|
|
169
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.
|
|
170
172
|
def handle_request_error(exception)
|
|
171
|
-
raise FulfilApi::
|
|
172
|
-
exception.message,
|
|
173
|
-
details: {
|
|
174
|
-
response_body: exception.response_body,
|
|
175
|
-
response_headers: exception.response_headers,
|
|
176
|
-
response_status: exception.response_status
|
|
177
|
-
}
|
|
178
|
-
)
|
|
173
|
+
raise FulfilApi::HttpError.from_faraday_error(exception)
|
|
179
174
|
end
|
|
180
175
|
|
|
181
176
|
# @param method [Symbol, String] The HTTP verb for the HTTP request.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FulfilApi
|
|
4
|
+
class Error
|
|
5
|
+
# The {FulfilApi::Error::Notifiable} publishes an {ActiveSupport::Notifications}
|
|
6
|
+
# event every time a {FulfilApi::Error} is raised. It gives an application a
|
|
7
|
+
# single place to report a failure of the Fulfil API to its APM — a counter of
|
|
8
|
+
# rate limit hits, an error tracker, a log line — without having to wrap every
|
|
9
|
+
# call into the gem in a `begin`/`rescue`.
|
|
10
|
+
#
|
|
11
|
+
# Ruby routes every form of `raise` through {Exception.exception} (when raising a
|
|
12
|
+
# class) or {Exception#exception} (when raising an instance), which makes those
|
|
13
|
+
# two methods the hook for "an error is on its way up the stack". Building an
|
|
14
|
+
# error without raising it — {FulfilApi::HttpError.from_faraday_error}, for
|
|
15
|
+
# example — publishes nothing.
|
|
16
|
+
#
|
|
17
|
+
# @example counting the errors of Fulfil in an APM
|
|
18
|
+
# # config/initializers/fulfil_api.rb
|
|
19
|
+
# FulfilApi.on_error do |error|
|
|
20
|
+
# Appsignal.increment_counter("fulfil_api_errors", 1, error: error.class.name)
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# @example subscribing through ActiveSupport directly, for the full payload
|
|
24
|
+
# ActiveSupport::Notifications.subscribe(FulfilApi::Error::EVENT_NAME) do |event|
|
|
25
|
+
# Rails.logger.warn("Fulfil responded with #{event.payload[:status_code]}")
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# @note A subscriber must not change the behaviour of the application it observes.
|
|
29
|
+
# An exception raised by a subscriber is therefore swallowed and reported on
|
|
30
|
+
# `$stderr` rather than allowed to replace the {FulfilApi::Error} on its way up.
|
|
31
|
+
module Notifiable
|
|
32
|
+
extend ActiveSupport::Concern
|
|
33
|
+
|
|
34
|
+
# The name of the published event. It follows the `<action>.<library>` naming
|
|
35
|
+
# convention of Rails, so an APM that subscribes to a whole library through a
|
|
36
|
+
# `/\.fulfil_api\z/` regexp picks it up along with everything the gem may
|
|
37
|
+
# instrument in the future.
|
|
38
|
+
EVENT_NAME = "error.fulfil_api"
|
|
39
|
+
|
|
40
|
+
# The key of the fiber local flag guarding against infinite recursion. A
|
|
41
|
+
# subscriber that itself raises a {FulfilApi::Error} — one calling back into
|
|
42
|
+
# Fulfil, for instance — would otherwise publish an event from within the
|
|
43
|
+
# publication of an event, without end.
|
|
44
|
+
REENTRANCY_KEY = :fulfil_api_notifying_error
|
|
45
|
+
|
|
46
|
+
class_methods do
|
|
47
|
+
# Publishes {EVENT_NAME} for `raise SomeError` and `raise SomeError, "message"`.
|
|
48
|
+
#
|
|
49
|
+
# @return [FulfilApi::Error] The error being raised.
|
|
50
|
+
def exception(*, **)
|
|
51
|
+
super.tap(&:notify)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Publishes {EVENT_NAME} for `raise error` and `raise error, "message"`.
|
|
56
|
+
#
|
|
57
|
+
# @return [FulfilApi::Error] The error being raised.
|
|
58
|
+
def exception(*)
|
|
59
|
+
super.tap(&:notify)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Publishes {EVENT_NAME} for the receiver.
|
|
63
|
+
#
|
|
64
|
+
# @return [void]
|
|
65
|
+
def notify
|
|
66
|
+
return if Thread.current[REENTRANCY_KEY]
|
|
67
|
+
|
|
68
|
+
Thread.current[REENTRANCY_KEY] = true
|
|
69
|
+
ActiveSupport::Notifications.instrument(EVENT_NAME, notification_payload)
|
|
70
|
+
rescue StandardError => e
|
|
71
|
+
warn "[FulfilApi] a subscriber of #{EVENT_NAME} raised #{e.class}: #{e.message}"
|
|
72
|
+
ensure
|
|
73
|
+
Thread.current[REENTRANCY_KEY] = nil
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
# The payload published alongside {EVENT_NAME}.
|
|
79
|
+
#
|
|
80
|
+
# `:exception` and `:exception_object` follow the convention Rails uses for its
|
|
81
|
+
# own instrumentation, which is what an APM looks for to attribute an event to
|
|
82
|
+
# a failure.
|
|
83
|
+
#
|
|
84
|
+
# @return [Hash]
|
|
85
|
+
def notification_payload
|
|
86
|
+
{ exception: [self.class.name, message], exception_object: self }
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
data/lib/fulfil_api/error.rb
CHANGED
|
@@ -3,7 +3,13 @@
|
|
|
3
3
|
module FulfilApi
|
|
4
4
|
# The {FulfilApi::Error} is the base class for all FulfilApi errors, also used
|
|
5
5
|
# for generic or unexpected errors.
|
|
6
|
+
#
|
|
7
|
+
# Raising any of them publishes an {ActiveSupport::Notifications} event, so an
|
|
8
|
+
# application can report the failure to its APM without rescuing every call into
|
|
9
|
+
# the gem. See {FulfilApi::Error::Notifiable} and {FulfilApi.on_error}.
|
|
6
10
|
class Error < StandardError
|
|
11
|
+
include Notifiable
|
|
12
|
+
|
|
7
13
|
attr_reader :details
|
|
8
14
|
|
|
9
15
|
# @param message [String] The displayable error message for the receiver.
|
|
@@ -32,4 +38,36 @@ module FulfilApi
|
|
|
32
38
|
nil
|
|
33
39
|
end
|
|
34
40
|
end
|
|
41
|
+
|
|
42
|
+
# Subscribes to every {FulfilApi::Error} raised by the gem.
|
|
43
|
+
#
|
|
44
|
+
# This is the shorthand for the common case: reporting the failure somewhere. To
|
|
45
|
+
# reach the HTTP status code, response body and response headers of a
|
|
46
|
+
# {FulfilApi::HttpError} as well, subscribe to {FulfilApi::Error::EVENT_NAME}
|
|
47
|
+
# through {ActiveSupport::Notifications} directly.
|
|
48
|
+
#
|
|
49
|
+
# @example incrementing a counter of an APM
|
|
50
|
+
# # config/initializers/fulfil_api.rb
|
|
51
|
+
# FulfilApi.on_error do |error|
|
|
52
|
+
# Appsignal.increment_counter("fulfil_api_errors", 1, error: error.class.name)
|
|
53
|
+
# end
|
|
54
|
+
#
|
|
55
|
+
# @example reporting only the rate limit hits
|
|
56
|
+
# FulfilApi.on_error do |error|
|
|
57
|
+
# next unless error.is_a?(FulfilApi::HttpError::TooManyRequests)
|
|
58
|
+
#
|
|
59
|
+
# Appsignal.increment_counter("fulfil_api_rate_limit_exceeded")
|
|
60
|
+
# end
|
|
61
|
+
#
|
|
62
|
+
# @note The block runs while the error travels up the stack, on the thread that
|
|
63
|
+
# raised it. Keep it cheap, and hand anything slow to a background job.
|
|
64
|
+
#
|
|
65
|
+
# @yieldparam error [FulfilApi::Error] The error being raised.
|
|
66
|
+
# @return [Object] The subscriber, to hand back to
|
|
67
|
+
# `ActiveSupport::Notifications.unsubscribe` to stop listening.
|
|
68
|
+
def self.on_error
|
|
69
|
+
ActiveSupport::Notifications.subscribe(Error::EVENT_NAME) do |event|
|
|
70
|
+
yield(event.payload[:exception_object])
|
|
71
|
+
end
|
|
72
|
+
end
|
|
35
73
|
end
|
|
@@ -0,0 +1,193 @@
|
|
|
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
|
+
|
|
178
|
+
private
|
|
179
|
+
|
|
180
|
+
# Enriches the payload published by {FulfilApi::Error::Notifiable} with what
|
|
181
|
+
# Fulfil reported, so a subscriber can tag its metrics by status code without
|
|
182
|
+
# having to unpack the exception itself.
|
|
183
|
+
#
|
|
184
|
+
# @return [Hash]
|
|
185
|
+
def notification_payload
|
|
186
|
+
super.merge(
|
|
187
|
+
response_body: response_body,
|
|
188
|
+
response_headers: response_headers,
|
|
189
|
+
status_code: status_code
|
|
190
|
+
)
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
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
|
|
@@ -182,15 +182,10 @@ module FulfilApi
|
|
|
182
182
|
|
|
183
183
|
# @param exception [Faraday::Error] Any error raised by Faraday during the execution
|
|
184
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.
|
|
185
187
|
def handle_request_error(exception)
|
|
186
|
-
raise FulfilApi::
|
|
187
|
-
exception.message,
|
|
188
|
-
details: {
|
|
189
|
-
response_body: exception.response_body,
|
|
190
|
-
response_headers: exception.response_headers,
|
|
191
|
-
response_status: exception.response_status
|
|
192
|
-
}
|
|
193
|
-
)
|
|
188
|
+
raise FulfilApi::HttpError.from_faraday_error(exception)
|
|
194
189
|
end
|
|
195
190
|
|
|
196
191
|
# @param method [Symbol, String] The HTTP verb for the HTTP request.
|
data/lib/fulfil_api/version.rb
CHANGED
data/lib/fulfil_api.rb
CHANGED
|
@@ -12,6 +12,7 @@ require "active_support/core_ext/hash/deep_merge"
|
|
|
12
12
|
require "active_support/core_ext/hash/indifferent_access"
|
|
13
13
|
require "active_support/core_ext/module/delegation"
|
|
14
14
|
require "active_support/core_ext/object/blank"
|
|
15
|
+
require "active_support/notifications"
|
|
15
16
|
|
|
16
17
|
module FulfilApi
|
|
17
18
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fulfil_api
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.7.
|
|
4
|
+
version: 0.7.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stefan Vermaas
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-08-18 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|
|
@@ -86,6 +86,8 @@ files:
|
|
|
86
86
|
- lib/fulfil_api/configuration.rb
|
|
87
87
|
- lib/fulfil_api/customer_shipment.rb
|
|
88
88
|
- lib/fulfil_api/error.rb
|
|
89
|
+
- lib/fulfil_api/error/notifiable.rb
|
|
90
|
+
- lib/fulfil_api/http_error.rb
|
|
89
91
|
- lib/fulfil_api/relation.rb
|
|
90
92
|
- lib/fulfil_api/relation/batchable.rb
|
|
91
93
|
- lib/fulfil_api/relation/countable.rb
|