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 +4 -4
- data/CHANGELOG.md +16 -5
- data/README.md +17 -1
- data/lib/grape/idempotency/middleware/error.rb +29 -9
- data/lib/grape/idempotency/version.rb +1 -1
- data/lib/grape/idempotency.rb +33 -10
- metadata +5 -5
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,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.
|
|
7
|
+
## [1.1.1] - 2026-10-15
|
|
8
8
|
|
|
9
9
|
### Fix
|
|
10
10
|
|
|
11
|
-
*
|
|
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
|
-
*
|
|
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):
|
|
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
|
-
|
|
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
|
@@ -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
|
-
|
|
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.
|
|
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, "
|
|
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.
|
|
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
|
|
@@ -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: []
|