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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 756cff52967b35d996913c3d682847a4cd362c2cbd4a95d8bf766c2f037d86ce
4
- data.tar.gz: bf87694a06895bfd09749676574a078f71b698f59c23211a438112b22f332f06
3
+ metadata.gz: be4536a211550d89d0c48cd4c83c7704cb5242bedd7028fe10037e1ceac11f76
4
+ data.tar.gz: 5fed5897f5416f70e99771aa7a773433420829acb60c9682fec55d943ac5080c
5
5
  SHA512:
6
- metadata.gz: bdaaa014a0a39fdbba3f850bdddbd27db5fca791f543609c4a847615eaeeac52d0dfb16e642d1310915bbdada5b46267291a1e43fec70299a027c5a9848285b2
7
- data.tar.gz: 2030a6016fcbaf64667b22a2be43af70239381e6d3311571e45c172d439bb67222ca57f2566becd847eb867d8ae9750ddcef70e1dd2444492191e2924f27e7cd
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
- ## Making idempotency key header mandatory ⚠️
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
@@ -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', '~> 1'
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
- raise NoMethodError, "undefined method '#{handler}'" unless respond_to?(handler)
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
- if endpoint
14
- response = handler.arity.zero? ? endpoint.instance_exec(&handler) : endpoint.instance_exec(error, &handler)
15
- else
16
- response = handler.arity.zero? ? instance_exec(&handler) : instance_exec(error, &handler)
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
- run_rescue_handler(:default_rescue_handler, Grape::Exceptions::InvalidResponse.new)
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Grape
4
4
  module Idempotency
5
- VERSION = '1.0.0'
5
+ VERSION = '1.1.1'
6
6
  end
7
7
  end
@@ -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
- cached_request = get_from_cache(idempotency_key)
37
- log(:debug, "Request has been found for the provided idempotency key => #{cached_request}") if cached_request
38
- if cached_request && (cached_request["params"] != grape.request.params || cached_request["path"] != grape.request.path)
39
- log(:debug, "Request has conflicts. Same params? => #{cached_request["params"] != grape.request.params}. Same path? => #{cached_request["path"] != grape.request.path}")
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 cached_request && cached_request["processing"] == true
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 cached_request
48
+ elsif stored_request
46
49
  log(:debug, "Returning the response from the original request.")
47
- grape.status cached_request["status"]
48
- grape.header(ORIGINAL_REQUEST_HEADER, cached_request["original_request"])
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 cached_request["response"]
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 !cached_request && !response
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
- log(:debug, "Error response stored.")
87
- grape.header(ORIGINAL_REQUEST_HEADER, original_request_id)
88
- grape.header(configuration.idempotency_key_header, stored_key)
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 !cached_request && response
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
- log(:debug, "Response stored.")
98
- grape.header(configuration.idempotency_key_header, stored_key)
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 && configuration.storage.respond_to?(:set)
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 get_from_cache(idempotency_key)
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
- request_with_error = JSON.parse(storage.get(key))
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.gsub(error_key_prefix, '')
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.0.0
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: 2023-11-23 00:00:00.000000000 Z
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: []