{"id":13878009,"url":"https://github.com/qonto/idempotent-request","last_synced_at":"2025-10-29T00:09:44.315Z","repository":{"id":38433328,"uuid":"118586712","full_name":"qonto/idempotent-request","owner":"qonto","description":"Rack middleware ensuring at most once requests for mutating endpoints.","archived":false,"fork":false,"pushed_at":"2024-08-04T10:42:53.000Z","size":44,"stargazers_count":59,"open_issues_count":1,"forks_count":14,"subscribers_count":11,"default_branch":"master","last_synced_at":"2025-10-17T07:29:49.536Z","etag":null,"topics":["idempotent","middleware","request","ruby"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/qonto.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-01-23T09:18:02.000Z","updated_at":"2025-03-21T07:09:20.000Z","dependencies_parsed_at":"2024-10-30T02:27:45.593Z","dependency_job_id":null,"html_url":"https://github.com/qonto/idempotent-request","commit_stats":{"total_commits":33,"total_committers":8,"mean_commits":4.125,"dds":"0.36363636363636365","last_synced_commit":"f49386bb9b3f432ab99d10ca4e3841a56000420d"},"previous_names":[],"tags_count":9,"template":false,"template_full_name":null,"purl":"pkg:github/qonto/idempotent-request","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/qonto%2Fidempotent-request","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/qonto%2Fidempotent-request/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/qonto%2Fidempotent-request/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/qonto%2Fidempotent-request/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/qonto","download_url":"https://codeload.github.com/qonto/idempotent-request/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/qonto%2Fidempotent-request/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":281064198,"owners_count":26437785,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-26T02:00:06.575Z","response_time":61,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["idempotent","middleware","request","ruby"],"created_at":"2024-08-06T08:01:37.409Z","updated_at":"2025-10-29T00:09:44.296Z","avatar_url":"https://github.com/qonto.png","language":"Ruby","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)\n\n# Idempotent Request\n\nRack middleware ensuring at most once requests for mutating endpoints.\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem 'idempotent-request'\n```\n\nAnd then execute:\n\n    $ bundle\n\nOr install it yourself as:\n\n    $ gem install idempotent-request\n\n## How it works\n\n1.  Front-end generates a unique `key` then a user goes to a specific route (for example, transfer page).\n2.  When user clicks \"Submit\" button, the `key` is sent in the header `idempotency-key` and back-end stores server response into redis.\n3.  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.\n4.  Once the user leaves or refreshes the page, front-end should re-generate the key.\n\n## Configuration\n```ruby\n# application.rb\nconfig.middleware.use IdempotentRequest::Middleware,\n  storage: IdempotentRequest::RedisStorage.new(::Redis.current, expire_time: 1.day),\n  policy: YOUR_CLASS\n```\n\nTo define a policy, whether a request should be idempotent, you have to provider a class with the following interface:\n\n```ruby\nclass Policy\n  attr_reader :request\n\n  def initialize(request)\n    @request = request\n  end\n\n  def should?\n    # request is Rack::Request class\n  end\nend\n```\n\n### Example of integration for rails\n\n\n```ruby\n# application.rb\nconfig.middleware.use IdempotentRequest::Middleware,\n  storage: IdempotentRequest::RedisStorage.new(::Redis.current, expire_time: 1.day),\n  policy: IdempotentRequest::Policy\n\nconfig.idempotent_routes = [\n  { controller: :'v1/transfers', action: :create },\n]\n```\n\n```ruby\n# lib/idempotent-request/policy.rb\nmodule IdempotentRequest\n  class Policy\n    attr_reader :request\n\n    def initialize(request)\n      @request = request\n    end\n\n    def should?\n      route = Rails.application.routes.recognize_path(request.path, method: request.request_method)\n      Rails.application.config.idempotent_routes.any? do |idempotent_route|\n        idempotent_route[:controller] == route[:controller].to_sym \u0026\u0026\n          idempotent_route[:action] == route[:action].to_sym\n      end\n    end\n  end\nend\n```\n\n\n### Use ActiveSupport::Notifications to read events\n\n```ruby\n# config/initializers/idempotent_request.rb\nActiveSupport::Notifications.subscribe('idempotent.request') do |name, start, finish, request_id, payload|\n  notification = payload[:request].env['idempotent.request']\n  if notification['read']\n    Rails.logger.info \"IdempotentRequest: Hit cached response from key #{notification['key']}, response: #{notification['read']}\"\n  elsif notification['write']\n    Rails.logger.info \"IdempotentRequest: Write: key #{notification['key']}, status: #{notification['write'][0]}, headers: #{notification['write'][1]}, unlocked? #{notification['unlocked']}\"\n  elsif notification['concurrent_request_response']\n    Rails.logger.warn \"IdempotentRequest: Concurrent request detected with key #{notification['key']}\"\n  end\nend\n```\n\n## Custom options\n\n```ruby\n# application.rb\nconfig.middleware.use IdempotentRequest::Middleware,\n  header_key: 'X-Qonto-Idempotency-Key', # by default Idempotency-key\n  policy: IdempotentRequest::Policy,\n  callback: IdempotentRequest::RailsCallback,\n  storage: IdempotentRequest::RedisStorage.new(::Redis.current, expire_time: 1.day, namespace: 'idempotency_keys'),\n  conflict_response_status: 409\n```\n\n### Policy\n\nCustom class to decide whether the request should be idempotent.\n\nSee *Example of integration for rails*\n\n### Storage\n\nWhere the response will be stored. Can be any class that implements the following interface:\n\n```ruby\ndef read(key)\n  # read from a storage\nend\n\ndef write(key, payload)\n  # write to a storage\nend\n```\n\n### Callback\n\nGet notified when a client sends a request with the same idempotency key:\n\n```ruby\nclass RailsCallback\n  attr_reader :request\n\n  def initialize(request)\n    @request = request\n  end\n\n  def detected(key:)\n    Rails.logger.warn \"IdempotentRequest request detected, key: #{key}\"\n  end\nend\n```\n\n### Conflict response status\n\nDefine http status code that should be returned when a client sends concurrent requests with the same idempotency key.\n\n## Contributing\n\nBug 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.\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).\n\n## Code of Conduct\n\nEveryone 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).\n\n\n## Releasing\n\nTo publish a new version to rubygems, update the version in `lib/version.rb`, and merge.\n","funding_links":[],"categories":["Ruby"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fqonto%2Fidempotent-request","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fqonto%2Fidempotent-request","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fqonto%2Fidempotent-request/lists"}