grape-idempotency 1.0.0 → 1.1.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 +26 -0
- data/README.md +67 -3
- data/grape-idempotency.gemspec +2 -1
- data/lib/grape/idempotency/middleware/error.rb +29 -9
- data/lib/grape/idempotency/version.rb +1 -1
- data/lib/grape/idempotency.rb +80 -26
- metadata +21 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: be4536a211550d89d0c48cd4c83c7704cb5242bedd7028fe10037e1ceac11f76
|
|
4
|
+
data.tar.gz: 5fed5897f5416f70e99771aa7a773433420829acb60c9682fec55d943ac5080c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 797f3ba565676d3eb97c5b0d38af871ec0c017b3f1a0705b08cd80c7f8b6fa30d32d3c921bb46aa2910827e06bb21500d3bf83e63c017177feb33e13bb5a0ab3
|
|
7
|
+
data.tar.gz: 071c4135447ceb9389cd2c5d8c2bb92477465318c523efc25eca43967e0c4533366a5d576cac3c52ee097c59ccd1164628124d73a5aea1072f6bfed7da3ae78b
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,32 @@ All changes to `grape-idempotency` will be documented in this file.
|
|
|
4
4
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [1.1.1] - 2026-10-15
|
|
8
|
+
|
|
9
|
+
### Fix
|
|
10
|
+
|
|
11
|
+
* Catch errors thrown with `error!` inside `rescue_from` handlers, which raised `UncaughtThrowError` with Grape >= 2.1 - [@jcagarcia](https://github.com/jcagarcia).
|
|
12
|
+
* Return Grape's `500 Invalid response` when a `rescue_from` handler does not return a valid response, instead of raising `NoMethodError: undefined method 'default_rescue_handler'` with Grape >= 2.1 - [@jcagarcia](https://github.com/jcagarcia).
|
|
13
|
+
* Store that `500 Invalid response` for the idempotency key, so retrying the request replays it instead of returning `409 Conflict` until the key expires - [@jcagarcia](https://github.com/jcagarcia).
|
|
14
|
+
* Support `rescue_from ..., with: :helper_method` with Grape >= 3.0, which resolves symbol handlers on the endpoint - [@jcagarcia](https://github.com/jcagarcia).
|
|
15
|
+
* Only treat a Hash returned by a `rescue_from` handler as an error response when it contains `:message`, `:status` and `:headers`, matching Grape - [@jcagarcia](https://github.com/jcagarcia).
|
|
16
|
+
* Handle `Grape::Exceptions::ErrorResponse` thrown by `error!` with Grape >= 3.3, both inside `idempotent` blocks and inside `rescue_from` handlers - [@jcagarcia](https://github.com/jcagarcia).
|
|
17
|
+
* [#22](https://github.com/jcagarcia/grape-idempotency/pull/22): Do not turn `rescue_from` responses into 500 errors when a stored error request expires or is invalid while being looked up. - [@salrepe](https://github.com/salrepe).
|
|
18
|
+
* [#22](https://github.com/jcagarcia/grape-idempotency/pull/22): Only access the storage from the error middleware for requests that stored an error inside `idempotent`, reading that exact key instead of scanning all error keys. - [@salrepe](https://github.com/salrepe).
|
|
19
|
+
* [#22](https://github.com/jcagarcia/grape-idempotency/pull/22): Do not store error requests when no idempotency key was provided. - [@salrepe](https://github.com/salrepe).
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
* [#24](https://github.com/jcagarcia/grape-idempotency/issues/24): Test Ruby (2.6 - 4.0) and Grape (1.5 - 4.0) in CI: the combinations where behaviour changes on every push and pull request, and every supported combination plus the latest Grape release weekly or on demand - [@jcagarcia](https://github.com/jcagarcia).
|
|
24
|
+
* Document Ruby and Grape compatibility in the README - [@jcagarcia](https://github.com/jcagarcia).
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
## [1.1.0] - 2023-12-12
|
|
28
|
+
|
|
29
|
+
### Feature
|
|
30
|
+
|
|
31
|
+
* [#20](https://github.com/jcagarcia/grape-idempotency/pull/20): Allow consumers to configure the gem for handling `Redis` exceptions - [@jcagarcia](https://github.com/jcagarcia).
|
|
32
|
+
|
|
7
33
|
## [1.0.0] - 2023-11-23
|
|
8
34
|
|
|
9
35
|
### Changed
|
data/README.md
CHANGED
|
@@ -10,9 +10,11 @@ Implementation based on the Active Internet-Draft [draft-ietf-httpapi-idempotenc
|
|
|
10
10
|
Topics covered in this README:
|
|
11
11
|
|
|
12
12
|
- [Installation](#installation-)
|
|
13
|
+
- [Compatibility](#compatibility-)
|
|
13
14
|
- [Basic Usage](#basic-usage-)
|
|
14
15
|
- [How it works](#how-it-works-)
|
|
15
|
-
- [Making idempotency key header mandatory](#making-idempotency-key-header-mandatory-)
|
|
16
|
+
- [Making idempotency key header mandatory](#making-idempotency-key-header-mandatory-)
|
|
17
|
+
- [Redis Storage Connectivity Issue](#redis-storage-connectivity-issue)
|
|
16
18
|
- [Configuration](#configuration-)
|
|
17
19
|
- [Changelog](#changelog)
|
|
18
20
|
- [Contributing](#contributing)
|
|
@@ -29,6 +31,20 @@ Ruby 2.6 or newer is required.
|
|
|
29
31
|
gem install grape-idempotency
|
|
30
32
|
```
|
|
31
33
|
|
|
34
|
+
## Compatibility 🤝
|
|
35
|
+
|
|
36
|
+
- Ruby 2.6 or newer.
|
|
37
|
+
- Grape 1.x and 2.x or newer are supported from a single codebase.
|
|
38
|
+
- Grape 1.0 - 1.4 are allowed by the gemspec but are not tested.
|
|
39
|
+
- Grape 1.x does not work with Rack 3, so if you are using Grape 1.x add `gem 'rack', '< 3'` to your `Gemfile`.
|
|
40
|
+
- Grape 1.5 does not load with ActiveSupport 7.1.x (7.0 and 7.2+ work). On Ruby 2.7 / 3.0, where 7.1 is the newest installable ActiveSupport, add `gem 'activesupport', '< 7.1'` to your `Gemfile`.
|
|
41
|
+
|
|
42
|
+
The tested Ruby and Grape combinations live in the [CI workflow](.github/workflows/ruby.yml): pushes and pull requests run the combinations where Ruby or Grape behaviour changes, and a weekly (or manually triggered) run tests every Grape minor line from 1.5 onwards against every Ruby version it supports, plus the latest Grape release on the newest Ruby. To run the specs against a specific Grape line locally, set `GRAPE_VERSION` (this rewrites your local `Gemfile.lock`, do not commit it):
|
|
43
|
+
|
|
44
|
+
```shell
|
|
45
|
+
GRAPE_VERSION="~> 1.8.0" bundle update grape && bundle exec rspec
|
|
46
|
+
```
|
|
47
|
+
|
|
32
48
|
## Basic Usage 📖
|
|
33
49
|
|
|
34
50
|
Configure the `Grape::Idempotency` class with a `Redis` instance.
|
|
@@ -81,7 +97,7 @@ Results are only saved if an API endpoint begins its execution. If incoming para
|
|
|
81
97
|
|
|
82
98
|
Additionally, this gem automatically appends the `Original-Request` header and the `Idempotency-Key` header to your API's response, enabling you to trace back to the initial request that generated that specific response.
|
|
83
99
|
|
|
84
|
-
|
|
100
|
+
### Making idempotency key header mandatory ⚠️
|
|
85
101
|
|
|
86
102
|
For some endpoints, you want to enforce your consumers to provide idempotency key. So, when wrapping the code inside the `idempotent` method, you can mark it as `required`:
|
|
87
103
|
|
|
@@ -113,6 +129,37 @@ If the Idempotency-Key request header is missing for a idempotent operation requ
|
|
|
113
129
|
|
|
114
130
|
If you want to change the error message returned in this scenario, check [How to configure idempotency key missing error message](#mandatory_header_response) section.
|
|
115
131
|
|
|
132
|
+
### Redis Storage Connectivity Issue
|
|
133
|
+
|
|
134
|
+
By default, `Redis` exceptions are not handled by the `grape-idempotency` gem.
|
|
135
|
+
|
|
136
|
+
Therefore, if an exception arises while attempting to read, write or delete data from the `Redis` storage, the gem will re-raise the identical exception to your application. Thus, you will be responsible for handling it within your own code, such as:
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
require 'grape'
|
|
140
|
+
require 'grape-idempotency'
|
|
141
|
+
|
|
142
|
+
class API < Grape::API
|
|
143
|
+
post '/payments' do
|
|
144
|
+
begin
|
|
145
|
+
idempotent do
|
|
146
|
+
status 201
|
|
147
|
+
Payment.create!({
|
|
148
|
+
amount: params[:amount]
|
|
149
|
+
})
|
|
150
|
+
end
|
|
151
|
+
rescue Redis::BaseError => e
|
|
152
|
+
error!("Redis error! Idempotency is very important here and we cannot continue.", 500)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
If you want to avoid this functionality, and you want the gem handles the potential `Redis` exceptions, you have the option to configure the gem for handling these `Redis` exceptions. Please refer to the [manage_redis_exceptions](#manage_redis_exceptions) configuration property.
|
|
160
|
+
|
|
161
|
+
🚨 WARNING: If a `Redis` exception appears AFTER performing the wrapped code, nothing will be re-raised. The process will continue working and the response will be returned to the consumer of your API. However, a `409 Conflict` response can be returned to your consumer if it retried the same call with the same idempotency key. This is because the gem was not able to associate the response of the original request to the original idempotency key because those connectivity issues.
|
|
162
|
+
|
|
116
163
|
## Configuration 🪚
|
|
117
164
|
|
|
118
165
|
In addition to the storage aspect, you have the option to supply additional configuration details to tailor the gem to the specific requirements of your project.
|
|
@@ -169,6 +216,7 @@ If you want to provide your own logger, you want to change the level to `DEBUG`
|
|
|
169
216
|
|
|
170
217
|
```ruby
|
|
171
218
|
Grape::Idempotency.configure do |c|
|
|
219
|
+
c.storage = @storage
|
|
172
220
|
c.logger = Infrastructure::MyLogger.new
|
|
173
221
|
c.logger_level = :debug
|
|
174
222
|
c.logger_prefix = '[my-own-prefix]'
|
|
@@ -194,6 +242,21 @@ I, [2023-11-23T22:41:39.148523 #1] DEBUG -- : [my-own-prefix] Request has been
|
|
|
194
242
|
I, [2023-11-23T22:41:39.148537 #1] DEBUG -- : [my-own-prefix] Returning the response from the original request.
|
|
195
243
|
```
|
|
196
244
|
|
|
245
|
+
### manage_redis_exceptions
|
|
246
|
+
|
|
247
|
+
By default, the `grape-idempotency` gem is configured to re-raise `Redis` exceptions.
|
|
248
|
+
|
|
249
|
+
If you want to delegate the `Redis` exception management into the gem, you can configure it using the `manage_redis_exceptions` configuration property.
|
|
250
|
+
|
|
251
|
+
```ruby
|
|
252
|
+
Grape::Idempotency.configure do |c|
|
|
253
|
+
c.storage = @storage
|
|
254
|
+
c.manage_redis_exceptions = true
|
|
255
|
+
end
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
However, this approach carries a certain level of risk. In the case that `Redis` experiences an outage, the idempotent functionality will be lost, the endpoint will behave as no idempotent, and this issue may go unnoticed.
|
|
259
|
+
|
|
197
260
|
### conflict_error_response
|
|
198
261
|
|
|
199
262
|
When providing a `Idempotency-Key: <key>` header, this gem compares incoming parameters to those of the original request (if exists) and returns a `409 - Conflict` status code if they don't match, preventing accidental misuse. The response body returned by the gem looks like:
|
|
@@ -320,4 +383,5 @@ Open issues on the GitHub issue tracker with clear information.
|
|
|
320
383
|
### Contributors
|
|
321
384
|
|
|
322
385
|
* Juan Carlos García - Creator - https://github.com/jcagarcia
|
|
323
|
-
* Carlos Cabanero - Contributor - https://github.com/Flip120
|
|
386
|
+
* Carlos Cabanero - Contributor - https://github.com/Flip120
|
|
387
|
+
* Salvador Revert Pedron - Contributor - https://github.com/salrepe
|
data/grape-idempotency.gemspec
CHANGED
|
@@ -22,7 +22,8 @@ Gem::Specification.new do |spec|
|
|
|
22
22
|
|
|
23
23
|
spec.required_ruby_version = '>= 2.6'
|
|
24
24
|
|
|
25
|
-
spec.add_runtime_dependency 'grape', '
|
|
25
|
+
spec.add_runtime_dependency 'grape', '>= 1'
|
|
26
|
+
spec.add_runtime_dependency 'redis', '>= 4'
|
|
26
27
|
|
|
27
28
|
spec.add_development_dependency 'bundler'
|
|
28
29
|
spec.add_development_dependency 'rspec'
|
|
@@ -5,36 +5,56 @@ module Grape
|
|
|
5
5
|
class Error < Base
|
|
6
6
|
def run_rescue_handler(handler, error, endpoint=nil)
|
|
7
7
|
if handler.instance_of?(Symbol)
|
|
8
|
-
|
|
8
|
+
# grape >= 3.0 resolves symbol handlers (`rescue_from ..., with: :helper`) on the endpoint
|
|
9
|
+
handler_owner = respond_to?(handler) || endpoint.nil? ? self : endpoint
|
|
10
|
+
raise NoMethodError, "undefined method '#{handler}'" unless handler_owner.respond_to?(handler)
|
|
9
11
|
|
|
10
|
-
handler = public_method(handler)
|
|
12
|
+
handler = handler_owner.public_method(handler)
|
|
11
13
|
end
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
response = catch(:error) do
|
|
16
|
+
if endpoint
|
|
17
|
+
handler.arity.zero? ? endpoint.instance_exec(&handler) : endpoint.instance_exec(error, &handler)
|
|
18
|
+
else
|
|
19
|
+
handler.arity.zero? ? instance_exec(&handler) : instance_exec(error, &handler)
|
|
20
|
+
end
|
|
17
21
|
end
|
|
18
22
|
|
|
23
|
+
response = error_response(response) if thrown_error?(response)
|
|
24
|
+
|
|
19
25
|
if response.is_a?(Rack::Response)
|
|
20
26
|
update_idempotency_error_with(error, response)
|
|
21
27
|
response
|
|
22
28
|
else
|
|
23
|
-
|
|
29
|
+
# default_rescue_handler is private since grape 2.1
|
|
30
|
+
response = run_rescue_handler(method(:default_rescue_handler), Grape::Exceptions::InvalidResponse.new, endpoint)
|
|
31
|
+
# Store it under the original error, the one the pending idempotent request was registered with
|
|
32
|
+
update_idempotency_error_with(error, response)
|
|
33
|
+
response
|
|
24
34
|
end
|
|
25
35
|
end
|
|
26
36
|
|
|
27
37
|
private
|
|
28
38
|
|
|
39
|
+
def thrown_error?(response)
|
|
40
|
+
# grape >= 3.3 throws Grape::Exceptions::ErrorResponse instead of a Hash
|
|
41
|
+
return true if defined?(Grape::Exceptions::ErrorResponse) && response.is_a?(Grape::Exceptions::ErrorResponse)
|
|
42
|
+
|
|
43
|
+
response.is_a?(Hash) && response.key?(:message) && response.key?(:status) && response.key?(:headers)
|
|
44
|
+
end
|
|
45
|
+
|
|
29
46
|
def update_idempotency_error_with(error, response)
|
|
47
|
+
stored_error_key = env && env[Grape::Idempotency::ERROR_KEY_ENV]
|
|
48
|
+
return unless stored_error_key
|
|
49
|
+
|
|
30
50
|
begin
|
|
31
51
|
body = JSON.parse(response.body.join)
|
|
32
52
|
rescue JSON::ParserError
|
|
33
53
|
body = response.body.join
|
|
34
54
|
end
|
|
35
55
|
|
|
36
|
-
Grape::Idempotency.update_error_with_rescue_from_result(error, response.status, body)
|
|
56
|
+
Grape::Idempotency.update_error_with_rescue_from_result(error, response.status, body, error_key: stored_error_key)
|
|
37
57
|
end
|
|
38
58
|
end
|
|
39
59
|
end
|
|
40
|
-
end
|
|
60
|
+
end
|
data/lib/grape/idempotency.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require 'grape'
|
|
2
|
+
require 'redis'
|
|
2
3
|
require 'logger'
|
|
3
4
|
require 'securerandom'
|
|
4
5
|
require 'grape/idempotency/version'
|
|
@@ -9,6 +10,8 @@ module Grape
|
|
|
9
10
|
autoload :Helpers, 'grape/idempotency/helpers'
|
|
10
11
|
Grape::Endpoint.send :include, Grape::Idempotency::Helpers
|
|
11
12
|
|
|
13
|
+
ERROR_KEY_ENV = "grape.idempotency.error_key".freeze
|
|
14
|
+
|
|
12
15
|
class << self
|
|
13
16
|
ORIGINAL_REQUEST_HEADER = "Original-Request".freeze
|
|
14
17
|
|
|
@@ -33,21 +36,21 @@ module Grape
|
|
|
33
36
|
grape.error!(configuration.mandatory_header_response, 400) if required && !idempotency_key
|
|
34
37
|
return block.call if !idempotency_key
|
|
35
38
|
|
|
36
|
-
|
|
37
|
-
log(:debug, "Request has been found for the provided idempotency key => #{
|
|
38
|
-
if
|
|
39
|
-
log(:debug, "Request has conflicts. Same params? => #{
|
|
39
|
+
stored_request = get_from_storage(idempotency_key)
|
|
40
|
+
log(:debug, "Request has been found for the provided idempotency key => #{stored_request}") if stored_request
|
|
41
|
+
if stored_request && (stored_request["params"] != grape.request.params || stored_request["path"] != grape.request.path)
|
|
42
|
+
log(:debug, "Request has conflicts. Same params? => #{stored_request["params"] != grape.request.params}. Same path? => #{stored_request["path"] != grape.request.path}")
|
|
40
43
|
log(:debug, "Returning conflict error response.")
|
|
41
44
|
grape.error!(configuration.conflict_error_response, 422)
|
|
42
|
-
elsif
|
|
45
|
+
elsif stored_request && stored_request["processing"] == true
|
|
43
46
|
log(:debug, "Returning processing error response.")
|
|
44
47
|
grape.error!(configuration.processing_response, 409)
|
|
45
|
-
elsif
|
|
48
|
+
elsif stored_request
|
|
46
49
|
log(:debug, "Returning the response from the original request.")
|
|
47
|
-
grape.status
|
|
48
|
-
grape.header(ORIGINAL_REQUEST_HEADER,
|
|
50
|
+
grape.status stored_request["status"]
|
|
51
|
+
grape.header(ORIGINAL_REQUEST_HEADER, stored_request["original_request"])
|
|
49
52
|
grape.header(configuration.idempotency_key_header, idempotency_key)
|
|
50
|
-
return
|
|
53
|
+
return stored_request["response"]
|
|
51
54
|
end
|
|
52
55
|
|
|
53
56
|
log(:debug, "Previous request information has NOT been found for the provided idempotency key.")
|
|
@@ -71,38 +74,45 @@ module Grape
|
|
|
71
74
|
|
|
72
75
|
if is_an_error?(response)
|
|
73
76
|
log(:debug, "An error response was returned by the performed block. => #{response}")
|
|
74
|
-
response = response[:message]
|
|
77
|
+
response = response.is_a?(Hash) ? response[:message] : response.message
|
|
75
78
|
end
|
|
76
79
|
|
|
77
80
|
grape.header(ORIGINAL_REQUEST_HEADER, original_request_id)
|
|
78
81
|
grape.body response
|
|
82
|
+
rescue Redis::BaseError => e
|
|
83
|
+
raise
|
|
79
84
|
rescue => e
|
|
80
85
|
log(:debug, "An unexpected error was raised when performing the block.")
|
|
81
|
-
if !
|
|
86
|
+
if idempotency_key && !stored_request && !response
|
|
82
87
|
validate_config!
|
|
83
88
|
log(:debug, "Storing error response.")
|
|
84
89
|
original_request_id = get_request_id(grape.request.headers)
|
|
85
90
|
stored_key = store_error_request(idempotency_key, grape.request.path, grape.request.params, grape.status, original_request_id, e)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
91
|
+
if stored_key
|
|
92
|
+
log(:debug, "Error response stored.")
|
|
93
|
+
grape.env[ERROR_KEY_ENV] = error_key(stored_key)
|
|
94
|
+
grape.header(ORIGINAL_REQUEST_HEADER, original_request_id)
|
|
95
|
+
grape.header(configuration.idempotency_key_header, stored_key)
|
|
96
|
+
end
|
|
89
97
|
end
|
|
90
98
|
log(:debug, "Re-raising the error.")
|
|
91
99
|
raise
|
|
92
100
|
ensure
|
|
93
|
-
if !
|
|
101
|
+
if !stored_request && response
|
|
94
102
|
validate_config!
|
|
95
103
|
log(:debug, "Storing response.")
|
|
96
104
|
stored_key = store_request_response(idempotency_key, grape.request.path, grape.request.params, grape.status, original_request_id, response)
|
|
97
|
-
|
|
98
|
-
|
|
105
|
+
if stored_key
|
|
106
|
+
log(:debug, "Response stored.")
|
|
107
|
+
grape.header(configuration.idempotency_key_header, stored_key)
|
|
108
|
+
end
|
|
99
109
|
end
|
|
100
110
|
end
|
|
101
111
|
|
|
102
|
-
def update_error_with_rescue_from_result(error, status, response)
|
|
112
|
+
def update_error_with_rescue_from_result(error, status, response, error_key: nil)
|
|
103
113
|
validate_config!
|
|
104
114
|
|
|
105
|
-
stored_error = get_error_request_for(error)
|
|
115
|
+
stored_error = error_key ? get_error_request_by_key(error_key) : get_error_request_for(error)
|
|
106
116
|
return unless stored_error
|
|
107
117
|
|
|
108
118
|
request_with_unmanaged_error = stored_error[:request]
|
|
@@ -113,6 +123,9 @@ module Grape
|
|
|
113
123
|
|
|
114
124
|
store_request_response(idempotency_key, path, params, status, original_request_id, response)
|
|
115
125
|
storage.del(stored_error[:error_key])
|
|
126
|
+
rescue Redis::BaseError => e
|
|
127
|
+
log(:error, "Storage error => #{e.message} - #{e}")
|
|
128
|
+
nil
|
|
116
129
|
end
|
|
117
130
|
|
|
118
131
|
private
|
|
@@ -122,7 +135,10 @@ module Grape
|
|
|
122
135
|
end
|
|
123
136
|
|
|
124
137
|
def valid_storage?
|
|
125
|
-
configuration.storage &&
|
|
138
|
+
configuration.storage &&
|
|
139
|
+
configuration.storage.respond_to?(:get) &&
|
|
140
|
+
configuration.storage.respond_to?(:set) &&
|
|
141
|
+
configuration.storage.respond_to?(:del)
|
|
126
142
|
end
|
|
127
143
|
|
|
128
144
|
def get_idempotency_key(headers)
|
|
@@ -141,11 +157,15 @@ module Grape
|
|
|
141
157
|
request_id || "req_#{SecureRandom.hex}"
|
|
142
158
|
end
|
|
143
159
|
|
|
144
|
-
def
|
|
160
|
+
def get_from_storage(idempotency_key)
|
|
145
161
|
value = storage.get(key(idempotency_key))
|
|
146
162
|
return unless value
|
|
147
163
|
|
|
148
164
|
JSON.parse(value)
|
|
165
|
+
rescue Redis::BaseError => e
|
|
166
|
+
log(:error, "Storage error => #{e.message} - #{e}")
|
|
167
|
+
return if configuration.manage_redis_exceptions
|
|
168
|
+
raise
|
|
149
169
|
end
|
|
150
170
|
|
|
151
171
|
def store_processing_request(idempotency_key, path, params, request_id)
|
|
@@ -157,6 +177,9 @@ module Grape
|
|
|
157
177
|
}
|
|
158
178
|
|
|
159
179
|
storage.set(key(idempotency_key), body.to_json, ex: configuration.expires_in, nx: true)
|
|
180
|
+
rescue Redis::BaseError => e
|
|
181
|
+
return true if configuration.manage_redis_exceptions
|
|
182
|
+
raise
|
|
160
183
|
end
|
|
161
184
|
|
|
162
185
|
def store_request_response(idempotency_key, path, params, status, request_id, response)
|
|
@@ -170,6 +193,9 @@ module Grape
|
|
|
170
193
|
|
|
171
194
|
storage.set(key(idempotency_key), body.to_json, ex: configuration.expires_in, nx: false)
|
|
172
195
|
|
|
196
|
+
idempotency_key
|
|
197
|
+
rescue Redis::BaseError => e
|
|
198
|
+
log(:error, "Storage error => #{e.message} - #{e}")
|
|
173
199
|
idempotency_key
|
|
174
200
|
end
|
|
175
201
|
|
|
@@ -187,29 +213,55 @@ module Grape
|
|
|
187
213
|
|
|
188
214
|
storage.set(error_key(idempotency_key), body, ex: 30, nx: false)
|
|
189
215
|
|
|
216
|
+
idempotency_key
|
|
217
|
+
rescue Redis::BaseError => e
|
|
218
|
+
log(:error, "Storage error => #{e.message} - #{e}")
|
|
190
219
|
idempotency_key
|
|
191
220
|
end
|
|
192
221
|
|
|
222
|
+
def get_error_request_by_key(error_key)
|
|
223
|
+
raw_request_with_error = storage.get(error_key)
|
|
224
|
+
return unless raw_request_with_error
|
|
225
|
+
|
|
226
|
+
{
|
|
227
|
+
error_key: error_key,
|
|
228
|
+
request: JSON.parse(raw_request_with_error),
|
|
229
|
+
idempotency_key: error_key.delete_prefix(error_key_prefix)
|
|
230
|
+
}
|
|
231
|
+
rescue Redis::BaseError, JSON::ParserError, TypeError => e
|
|
232
|
+
log(:error, "Error retrieving stored error request => #{e.message} - #{e}")
|
|
233
|
+
nil
|
|
234
|
+
end
|
|
235
|
+
|
|
193
236
|
def get_error_request_for(error)
|
|
194
237
|
error_keys = storage.keys("#{error_key_prefix}*")
|
|
195
238
|
return if error_keys.empty?
|
|
196
239
|
|
|
197
240
|
error_keys.map do |key|
|
|
198
|
-
|
|
241
|
+
raw_request_with_error = storage.get(key)
|
|
242
|
+
next unless raw_request_with_error
|
|
243
|
+
|
|
244
|
+
request_with_error = JSON.parse(raw_request_with_error)
|
|
199
245
|
error_class_name = request_with_error["error"]["class_name"]
|
|
200
246
|
error_message = request_with_error["error"]["message"]
|
|
201
|
-
|
|
247
|
+
|
|
202
248
|
if error_class_name == error.class.to_s && error_message == error.message
|
|
203
249
|
{
|
|
204
250
|
error_key: key,
|
|
205
251
|
request: request_with_error,
|
|
206
|
-
idempotency_key: key.
|
|
252
|
+
idempotency_key: key.delete_prefix(error_key_prefix)
|
|
207
253
|
}
|
|
208
254
|
end
|
|
209
|
-
end.first
|
|
255
|
+
end.compact.first
|
|
256
|
+
rescue Redis::BaseError, JSON::ParserError, TypeError => e
|
|
257
|
+
log(:error, "Error retrieving stored error request => #{e.message} - #{e}")
|
|
258
|
+
nil
|
|
210
259
|
end
|
|
211
260
|
|
|
212
261
|
def is_an_error?(response)
|
|
262
|
+
# grape >= 3.3 throws Grape::Exceptions::ErrorResponse instead of a Hash
|
|
263
|
+
return true if defined?(Grape::Exceptions::ErrorResponse) && response.is_a?(Grape::Exceptions::ErrorResponse)
|
|
264
|
+
|
|
213
265
|
response.is_a?(Hash) && response.has_key?(:message) && response.has_key?(:headers) && response.has_key?(:status)
|
|
214
266
|
end
|
|
215
267
|
|
|
@@ -252,7 +304,8 @@ module Grape
|
|
|
252
304
|
|
|
253
305
|
class Configuration
|
|
254
306
|
attr_accessor :storage, :logger, :logger_level, :logger_prefix, :expires_in, :idempotency_key_header,
|
|
255
|
-
:request_id_header, :conflict_error_response, :processing_response, :mandatory_header_response
|
|
307
|
+
:request_id_header, :conflict_error_response, :processing_response, :mandatory_header_response,
|
|
308
|
+
:manage_redis_exceptions
|
|
256
309
|
|
|
257
310
|
class Error < StandardError; end
|
|
258
311
|
|
|
@@ -264,6 +317,7 @@ module Grape
|
|
|
264
317
|
@expires_in = 216_000
|
|
265
318
|
@idempotency_key_header = "idempotency-key"
|
|
266
319
|
@request_id_header = "x-request-id"
|
|
320
|
+
@manage_redis_exceptions = false
|
|
267
321
|
@conflict_error_response = {
|
|
268
322
|
"title" => "Idempotency-Key is already used",
|
|
269
323
|
"detail" => "This operation is idempotent and it requires correct usage of Idempotency Key. Idempotency Key MUST not be reused across different payloads of this operation."
|
metadata
CHANGED
|
@@ -1,29 +1,43 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: grape-idempotency
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Juan Carlos García
|
|
8
|
-
autorequire:
|
|
8
|
+
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-10-15 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: grape
|
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
|
16
16
|
requirements:
|
|
17
|
-
- - "
|
|
17
|
+
- - ">="
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
19
|
version: '1'
|
|
20
20
|
type: :runtime
|
|
21
21
|
prerelease: false
|
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
23
|
requirements:
|
|
24
|
-
- - "
|
|
24
|
+
- - ">="
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
26
|
version: '1'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: redis
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '4'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '4'
|
|
27
41
|
- !ruby/object:Gem::Dependency
|
|
28
42
|
name: bundler
|
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -104,7 +118,7 @@ homepage: https://github.com/jcagarcia/grape-idempotency
|
|
|
104
118
|
licenses:
|
|
105
119
|
- MIT
|
|
106
120
|
metadata: {}
|
|
107
|
-
post_install_message:
|
|
121
|
+
post_install_message:
|
|
108
122
|
rdoc_options: []
|
|
109
123
|
require_paths:
|
|
110
124
|
- lib
|
|
@@ -120,7 +134,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
120
134
|
version: '0'
|
|
121
135
|
requirements: []
|
|
122
136
|
rubygems_version: 3.4.10
|
|
123
|
-
signing_key:
|
|
137
|
+
signing_key:
|
|
124
138
|
specification_version: 4
|
|
125
139
|
summary: Gem for supporting idempotency in your Grape APIs
|
|
126
140
|
test_files: []
|