Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/qonto/idempotent-request
Rack middleware ensuring at most once requests for mutating endpoints.
https://github.com/qonto/idempotent-request
idempotent middleware request ruby
Last synced: 6 days ago
JSON representation
Rack middleware ensuring at most once requests for mutating endpoints.
- Host: GitHub
- URL: https://github.com/qonto/idempotent-request
- Owner: qonto
- License: mit
- Created: 2018-01-23T09:18:02.000Z (about 7 years ago)
- Default Branch: master
- Last Pushed: 2024-08-04T10:42:53.000Z (6 months ago)
- Last Synced: 2025-01-15T07:46:45.748Z (13 days ago)
- Topics: idempotent, middleware, request, ruby
- Language: Ruby
- Homepage:
- Size: 43 KB
- Stars: 57
- Watchers: 13
- Forks: 13
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE.txt
- Code of conduct: CODE_OF_CONDUCT.md
Awesome Lists containing this project
README
![Gem Version](https://badge.fury.io/rb/idempotent-request.svg) ![CI Status](https://github.com/qonto/idempotent-request/actions/workflows/tests.yml/badge.svg)
# Idempotent Request
Rack middleware ensuring at most once requests for mutating endpoints.
## Installation
Add this line to your application's Gemfile:
```ruby
gem 'idempotent-request'
```And then execute:
$ bundle
Or install it yourself as:
$ gem install idempotent-request
## How it works
1. Front-end generates a unique `key` then a user goes to a specific route (for example, transfer page).
2. When user clicks "Submit" button, the `key` is sent in the header `idempotency-key` and back-end stores server response into redis.
3. All the consecutive requests with the `key` won't be executer by the server and the result of previous response (2) will be fetched from redis.
4. Once the user leaves or refreshes the page, front-end should re-generate the key.## Configuration
```ruby
# application.rb
config.middleware.use IdempotentRequest::Middleware,
storage: IdempotentRequest::RedisStorage.new(::Redis.current, expire_time: 1.day),
policy: YOUR_CLASS
```To define a policy, whether a request should be idempotent, you have to provider a class with the following interface:
```ruby
class Policy
attr_reader :requestdef initialize(request)
@request = request
enddef should?
# request is Rack::Request class
end
end
```### Example of integration for rails
```ruby
# application.rb
config.middleware.use IdempotentRequest::Middleware,
storage: IdempotentRequest::RedisStorage.new(::Redis.current, expire_time: 1.day),
policy: IdempotentRequest::Policyconfig.idempotent_routes = [
{ controller: :'v1/transfers', action: :create },
]
``````ruby
# lib/idempotent-request/policy.rb
module IdempotentRequest
class Policy
attr_reader :requestdef initialize(request)
@request = request
enddef should?
route = Rails.application.routes.recognize_path(request.path, method: request.request_method)
Rails.application.config.idempotent_routes.any? do |idempotent_route|
idempotent_route[:controller] == route[:controller].to_sym &&
idempotent_route[:action] == route[:action].to_sym
end
end
end
end
```### Use ActiveSupport::Notifications to read events
```ruby
# config/initializers/idempotent_request.rb
ActiveSupport::Notifications.subscribe('idempotent.request') do |name, start, finish, request_id, payload|
notification = payload[:request].env['idempotent.request']
if notification['read']
Rails.logger.info "IdempotentRequest: Hit cached response from key #{notification['key']}, response: #{notification['read']}"
elsif notification['write']
Rails.logger.info "IdempotentRequest: Write: key #{notification['key']}, status: #{notification['write'][0]}, headers: #{notification['write'][1]}, unlocked? #{notification['unlocked']}"
elsif notification['concurrent_request_response']
Rails.logger.warn "IdempotentRequest: Concurrent request detected with key #{notification['key']}"
end
end
```## Custom options
```ruby
# application.rb
config.middleware.use IdempotentRequest::Middleware,
header_key: 'X-Qonto-Idempotency-Key', # by default Idempotency-key
policy: IdempotentRequest::Policy,
callback: IdempotentRequest::RailsCallback,
storage: IdempotentRequest::RedisStorage.new(::Redis.current, expire_time: 1.day, namespace: 'idempotency_keys'),
conflict_response_status: 409
```### Policy
Custom class to decide whether the request should be idempotent.
See *Example of integration for rails*
### Storage
Where the response will be stored. Can be any class that implements the following interface:
```ruby
def read(key)
# read from a storage
enddef write(key, payload)
# write to a storage
end
```### Callback
Get notified when a client sends a request with the same idempotency key:
```ruby
class RailsCallback
attr_reader :requestdef initialize(request)
@request = request
enddef detected(key:)
Rails.logger.warn "IdempotentRequest request detected, key: #{key}"
end
end
```### Conflict response status
Define http status code that should be returned when a client sends concurrent requests with the same idempotency key.
## Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/idempotent-request. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
## License
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
## Code of Conduct
Everyone interacting in the Idempotent::Request project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/idempotent-request/blob/master/CODE_OF_CONDUCT.md).
## Releasing
To publish a new version to rubygems, update the version in `lib/version.rb`, and merge.