grape-idempotency 1.1.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: 115100d8cd1141c7c801a1501ede2db9eff3419a5b2974de93c4b79e7c812fd2
4
- data.tar.gz: 567472c4ab8ad2aae2561d7af0e808c803a025caae34f75492c2b889b6468d7e
3
+ metadata.gz: be4536a211550d89d0c48cd4c83c7704cb5242bedd7028fe10037e1ceac11f76
4
+ data.tar.gz: 5fed5897f5416f70e99771aa7a773433420829acb60c9682fec55d943ac5080c
5
5
  SHA512:
6
- metadata.gz: fde07abe226552ba88086f20fcece9778ef13ee04c17c27376aea2d13386955944caea65ae28ff1d2abdda72bc501600a9afb107229e6da47b4dd02a56b2e5f5
7
- data.tar.gz: 3a9a9fcd8f30d5f1deab9da2c719c54c1a0cbecafb5ce3b1196301bc60983bd3271153f4b02fed795d49d4188255507e0a8445d1fdd37cad1bbe0a0851b32e48
6
+ metadata.gz: 797f3ba565676d3eb97c5b0d38af871ec0c017b3f1a0705b08cd80c7f8b6fa30d32d3c921bb46aa2910827e06bb21500d3bf83e63c017177feb33e13bb5a0ab3
7
+ data.tar.gz: 071c4135447ceb9389cd2c5d8c2bb92477465318c523efc25eca43967e0c4533366a5d576cac3c52ee097c59ccd1164628124d73a5aea1072f6bfed7da3ae78b
data/CHANGELOG.md CHANGED
@@ -4,20 +4,31 @@ 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.0] - (Next)
7
+ ## [1.1.1] - 2026-10-15
8
8
 
9
9
  ### Fix
10
10
 
11
- * Your contribution here.
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).
12
20
 
13
21
  ### Changed
14
22
 
15
- * Your contribution here.
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
16
28
 
17
29
  ### Feature
18
30
 
19
- * [#20](https://github.com/jcagarcia/grape-idempotency/pull/20): Manage `Redis` exceptions by default and allow the gem's consumer to manage them by itself - [@jcagarcia](https://github.com/jcagarcia).
20
- * Your contribution here.
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).
21
32
 
22
33
  ## [1.0.0] - 2023-11-23
23
34
 
data/README.md CHANGED
@@ -10,6 +10,7 @@ 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
16
  - [Making idempotency key header mandatory](#making-idempotency-key-header-mandatory-)
@@ -30,6 +31,20 @@ Ruby 2.6 or newer is required.
30
31
  gem install grape-idempotency
31
32
  ```
32
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
+
33
48
  ## Basic Usage 📖
34
49
 
35
50
  Configure the `Grape::Idempotency` class with a `Redis` instance.
@@ -368,4 +383,5 @@ Open issues on the GitHub issue tracker with clear information.
368
383
  ### Contributors
369
384
 
370
385
  * Juan Carlos García - Creator - https://github.com/jcagarcia
371
- * Carlos Cabanero - Contributor - https://github.com/Flip120
386
+ * Carlos Cabanero - Contributor - https://github.com/Flip120
387
+ * Salvador Revert Pedron - Contributor - https://github.com/salrepe
@@ -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.1.0'
5
+ VERSION = '1.1.1'
6
6
  end
7
7
  end
@@ -10,6 +10,8 @@ module Grape
10
10
  autoload :Helpers, 'grape/idempotency/helpers'
11
11
  Grape::Endpoint.send :include, Grape::Idempotency::Helpers
12
12
 
13
+ ERROR_KEY_ENV = "grape.idempotency.error_key".freeze
14
+
13
15
  class << self
14
16
  ORIGINAL_REQUEST_HEADER = "Original-Request".freeze
15
17
 
@@ -72,7 +74,7 @@ module Grape
72
74
 
73
75
  if is_an_error?(response)
74
76
  log(:debug, "An error response was returned by the performed block. => #{response}")
75
- response = response[:message]
77
+ response = response.is_a?(Hash) ? response[:message] : response.message
76
78
  end
77
79
 
78
80
  grape.header(ORIGINAL_REQUEST_HEADER, original_request_id)
@@ -81,13 +83,14 @@ module Grape
81
83
  raise
82
84
  rescue => e
83
85
  log(:debug, "An unexpected error was raised when performing the block.")
84
- if !stored_request && !response
86
+ if idempotency_key && !stored_request && !response
85
87
  validate_config!
86
88
  log(:debug, "Storing error response.")
87
89
  original_request_id = get_request_id(grape.request.headers)
88
90
  stored_key = store_error_request(idempotency_key, grape.request.path, grape.request.params, grape.status, original_request_id, e)
89
91
  if stored_key
90
92
  log(:debug, "Error response stored.")
93
+ grape.env[ERROR_KEY_ENV] = error_key(stored_key)
91
94
  grape.header(ORIGINAL_REQUEST_HEADER, original_request_id)
92
95
  grape.header(configuration.idempotency_key_header, stored_key)
93
96
  end
@@ -106,10 +109,10 @@ module Grape
106
109
  end
107
110
  end
108
111
 
109
- def update_error_with_rescue_from_result(error, status, response)
112
+ def update_error_with_rescue_from_result(error, status, response, error_key: nil)
110
113
  validate_config!
111
114
 
112
- stored_error = get_error_request_for(error)
115
+ stored_error = error_key ? get_error_request_by_key(error_key) : get_error_request_for(error)
113
116
  return unless stored_error
114
117
 
115
118
  request_with_unmanaged_error = stored_error[:request]
@@ -216,29 +219,49 @@ module Grape
216
219
  idempotency_key
217
220
  end
218
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
+
219
236
  def get_error_request_for(error)
220
237
  error_keys = storage.keys("#{error_key_prefix}*")
221
238
  return if error_keys.empty?
222
239
 
223
240
  error_keys.map do |key|
224
- 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)
225
245
  error_class_name = request_with_error["error"]["class_name"]
226
246
  error_message = request_with_error["error"]["message"]
227
-
247
+
228
248
  if error_class_name == error.class.to_s && error_message == error.message
229
249
  {
230
250
  error_key: key,
231
251
  request: request_with_error,
232
- idempotency_key: key.gsub(error_key_prefix, '')
252
+ idempotency_key: key.delete_prefix(error_key_prefix)
233
253
  }
234
254
  end
235
- end.first
236
- rescue Redis::BaseError => e
237
- log(:error, "Storage error => #{e.message} - #{e}")
255
+ end.compact.first
256
+ rescue Redis::BaseError, JSON::ParserError, TypeError => e
257
+ log(:error, "Error retrieving stored error request => #{e.message} - #{e}")
238
258
  nil
239
259
  end
240
260
 
241
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
+
242
265
  response.is_a?(Hash) && response.has_key?(:message) && response.has_key?(:headers) && response.has_key?(:status)
243
266
  end
244
267
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: grape-idempotency
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.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-12-12 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
@@ -118,7 +118,7 @@ homepage: https://github.com/jcagarcia/grape-idempotency
118
118
  licenses:
119
119
  - MIT
120
120
  metadata: {}
121
- post_install_message:
121
+ post_install_message:
122
122
  rdoc_options: []
123
123
  require_paths:
124
124
  - lib
@@ -134,7 +134,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
134
134
  version: '0'
135
135
  requirements: []
136
136
  rubygems_version: 3.4.10
137
- signing_key:
137
+ signing_key:
138
138
  specification_version: 4
139
139
  summary: Gem for supporting idempotency in your Grape APIs
140
140
  test_files: []