Idempotent Request
Rack middleware ensuring at most once requests for mutating endpoints.
Installation
Add this line to your application's Gemfile:
gem 'idempotent-request'
And then execute:
$ bundle
Or install it yourself as:
$ gem install idempotent-request
How it works
- Front-end generates a unique
key
then a user goes to a specific route (for example, transfer page). - When user clicks "Submit" button, the
key
is sent in the headeridempotency-key
and back-end stores server response into redis. - 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. - Once the user leaves or refreshes the page, front-end should re-generate the key.
Configuration
# 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:
class Policy
attr_reader :request
def initialize(request)
@request = request
end
def should?
# request is Rack::Request class
end
end
Example of integration for rails
# application.rb
config.middleware.use IdempotentRequest::Middleware,
storage: IdempotentRequest::RedisStorage.new(::Redis.current, expire_time: 1.day),
policy: IdempotentRequest::Policy
config.idempotent_routes = [
{ controller: :'v1/transfers', action: :create },
]
# lib/idempotent-request/policy.rb
module IdempotentRequest
class Policy
attr_reader :request
def initialize(request)
@request = request
end
def 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
# 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
# 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:
def read(key)
# read from a storage
end
def write(key, payload)
# write to a storage
end
Callback
Get notified when a client sends a request with the same idempotency key:
class RailsCallback
attr_reader :request
def initialize(request)
@request = request
end
def 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 code of conduct.
License
The gem is available as open source under the terms of the MIT License.
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.
Releasing
To publish a new version to rubygems, update the version in lib/version.rb
, and merge.