Enable idempotent operations in POST and PATCH endpoints

Overview

tests pypi python-versions codecov

Idempotency Header ASGI Middleware

A middleware for making POST and PATCH endpoints idempotent.

The purpose of the middleware is to guarantee that execution of mutating endpoints happens exactly once, regardless of the number of requests. We achieve this by caching responses, and returning already-saved responses to the user on repeated requests. Responses are only cached when an idempotency-key HTTP header is present, so clients must opt-into this behaviour.

This is largely modelled after stripe' implementation.

The middleware is compatible with both Starlette and FastAPI apps.

Installation

pip install asgi-idempotency-header

Setup

Add the middleware to your app like this:

from fastapi import FastAPI

from idempotency_header_middleware import IdempotencyHeaderMiddleware
from idempotency_header_middleware.backends import AioredisBackend


backend = AioredisBackend(redis=redis)

app = FastAPI()
app.add_middleware(IdempotencyHeaderMiddleware(backend=backend))

or like this:

from fastapi import FastAPI
from fastapi.middleware import Middleware

from idempotency_header_middleware import IdempotencyHeaderMiddleware
from idempotency_header_middleware.backends import AioredisBackend


backend = AioredisBackend(redis=redis)

app = FastAPI(
    middleware=[
        Middleware(
            IdempotencyHeaderMiddleware,
            backend=backend,
        )
    ]
)

If you're using Starlette, just substitute FastAPI for Starlette and it should work the same.

Configuration

The middleware takes a few arguments. A full example looks like this:

from aioredis import from_url

from idempotency_header_middleware import IdempotencyHeaderMiddleware
from idempotency_header_middleware.backends import AioredisBackend


redis = from_url(redis_url)
backend = AioredisBackend(redis=redis)

IdempotencyHeaderMiddleware(
    backend,
    idempotency_header_key='Idempotency-Key',
    replay_header_key='Idempotent-Replayed',
    enforce_uuid4_formatting=False,
    expiry=60 * 60 * 24,
)

The following section describes each argument:

Backend

from idempotency_header_middleware.backends import AioredisBackend, MemoryBackend

backend: Union[AioredisBackend, MemoryBackend]

The backend is the only required argument, as it defines how and where to store a response.

The package comes with an aioredis backend implementation, and a memory-backend for testing.

Contributions for more backends are welcomed, and configuring a custom backend is pretty simple - just take a look at the existing ones.

Idempotency header key

idempotency_header_key: str = 'Idempotency-Key'

The idempotency header key is the header value to check for. When present, the middleware is used.

The default value is "Idempotency-Key", but it can be defined as any string.

Replay header key

replay_header_key: str = 'Idempotent-Replayed'

The replay header is added to replayed responses. It provides a way for the client to tell whether the action was performed for the first time or not.

Enforce UUID formatting

enforce_uuid4_formatting: bool = False

Convenience option for stricter header value validation.

Clients can technically set any value they want in their header, but the shorter the key value is, the higher the risk of value-collisions is from other users. If two users accidentally send in the same header value for what's meant to be two separate requests, the middleware will interpret them as the same.

By enabling this option, you can force users to use UUIDs as header values, and pretty much eliminate this risk.

When validation fails, a 422 response is returned from the middleware, informing the user that the header value is malformed.

Expiry

expiry: int = 60 * 60 * 24

How long to cache responses for, measured in seconds. Set to 24 hours by default.

Quick summary of behaviours

Briefly summarized, this is how the middleware functions:

  • The first request is processed, and consequent requests are replayed, until the response expires. expiry can be set to None to skip expiry, but most likely you will want to expire responses after a while.
  • If two requests comes in at the same time - i.e., if a second request hits the middlware before the first request has finished, the middleware will return a 409, informing the user that a request is being processed, and that we cannot handle the second request.
  • The middleware only handles HTTP requests.
  • The middleware only handles requests with POST and PATCH methods. Other HTTP methods are idempotent by default.
Comments
  • Update redis backend with fixes V1.1

    Update redis backend with fixes V1.1

    The changes from V1 with a few test/bug fixes.

    • Fix RedisBackend.store_idempotency_key() to match the spec outlined in the parent Backend: If the key already exists, return True, when the key doesn't exist, return False
    • Fix RedisBackend.__init__ to take in expiry (The @dataclass decorator won't override an existing __init__)
    • Adjust poetry extras to install redis not aioredis
    • Re-add the client test fixture as a async fixture
    opened by PatrickGleeson 9
  • Allow configuration of which HTTP methods to cache and replay.

    Allow configuration of which HTTP methods to cache and replay.

    In non-RESTful applications, it is useful to allow caching additional HTTP methods beyond POST and PATCH. This pull request allows passing a list of HTTP method names for which caching and replaying will be enabled.

    opened by PatrickGleeson 6
  • Switch to redis.asyncio from aioredis

    Switch to redis.asyncio from aioredis

    Renamed the backend class to RedisBackend and made AioredisBackend an alternate name for backwards compatibility.

    Also cleaned up some tests and setting a custom expiry.

    opened by PatrickGleeson 3
  • Bump codecov/codecov-action from 2 to 3

    Bump codecov/codecov-action from 2 to 3

    Bumps codecov/codecov-action from 2 to 3.

    Release notes

    Sourced from codecov/codecov-action's releases.

    v3.0.0

    Breaking Changes

    • #689 Bump to node16 and small fixes

    Features

    • #688 Incorporate gcov arguments for the Codecov uploader

    Dependencies

    • #548 build(deps-dev): bump jest-junit from 12.2.0 to 13.0.0
    • #603 [Snyk] Upgrade @​actions/core from 1.5.0 to 1.6.0
    • #628 build(deps): bump node-fetch from 2.6.1 to 3.1.1
    • #634 build(deps): bump node-fetch from 3.1.1 to 3.2.0
    • #636 build(deps): bump openpgp from 5.0.1 to 5.1.0
    • #652 build(deps-dev): bump @​vercel/ncc from 0.30.0 to 0.33.3
    • #653 build(deps-dev): bump @​types/node from 16.11.21 to 17.0.18
    • #659 build(deps-dev): bump @​types/jest from 27.4.0 to 27.4.1
    • #667 build(deps): bump actions/checkout from 2 to 3
    • #673 build(deps): bump node-fetch from 3.2.0 to 3.2.3
    • #683 build(deps): bump minimist from 1.2.5 to 1.2.6
    • #685 build(deps): bump @​actions/github from 5.0.0 to 5.0.1
    • #681 build(deps-dev): bump @​types/node from 17.0.18 to 17.0.23
    • #682 build(deps-dev): bump typescript from 4.5.5 to 4.6.3
    • #676 build(deps): bump @​actions/exec from 1.1.0 to 1.1.1
    • #675 build(deps): bump openpgp from 5.1.0 to 5.2.1

    v2.1.0

    2.1.0

    Features

    • #515 Allow specifying version of Codecov uploader

    Dependencies

    • #499 build(deps-dev): bump @​vercel/ncc from 0.29.0 to 0.30.0
    • #508 build(deps): bump openpgp from 5.0.0-5 to 5.0.0
    • #514 build(deps-dev): bump @​types/node from 16.6.0 to 16.9.0

    v2.0.3

    2.0.3

    Fixes

    • #464 Fix wrong link in the readme
    • #485 fix: Add override OS and linux default to platform

    Dependencies

    • #447 build(deps): bump openpgp from 5.0.0-4 to 5.0.0-5
    • #458 build(deps-dev): bump eslint from 7.31.0 to 7.32.0
    • #465 build(deps-dev): bump @​typescript-eslint/eslint-plugin from 4.28.4 to 4.29.1
    • #466 build(deps-dev): bump @​typescript-eslint/parser from 4.28.4 to 4.29.1
    • #468 build(deps-dev): bump @​types/jest from 26.0.24 to 27.0.0
    • #470 build(deps-dev): bump @​types/node from 16.4.0 to 16.6.0
    • #472 build(deps): bump path-parse from 1.0.6 to 1.0.7
    • #473 build(deps-dev): bump @​types/jest from 27.0.0 to 27.0.1

    ... (truncated)

    Changelog

    Sourced from codecov/codecov-action's changelog.

    3.1.0

    Features

    • #699 Incorporate xcode arguments for the Codecov uploader

    Dependencies

    • #694 build(deps-dev): bump @​vercel/ncc from 0.33.3 to 0.33.4
    • #696 build(deps-dev): bump @​types/node from 17.0.23 to 17.0.25
    • #698 build(deps-dev): bump jest-junit from 13.0.0 to 13.2.0

    3.0.0

    Breaking Changes

    • #689 Bump to node16 and small fixes

    Features

    • #688 Incorporate gcov arguments for the Codecov uploader

    Dependencies

    • #548 build(deps-dev): bump jest-junit from 12.2.0 to 13.0.0
    • #603 [Snyk] Upgrade @​actions/core from 1.5.0 to 1.6.0
    • #628 build(deps): bump node-fetch from 2.6.1 to 3.1.1
    • #634 build(deps): bump node-fetch from 3.1.1 to 3.2.0
    • #636 build(deps): bump openpgp from 5.0.1 to 5.1.0
    • #652 build(deps-dev): bump @​vercel/ncc from 0.30.0 to 0.33.3
    • #653 build(deps-dev): bump @​types/node from 16.11.21 to 17.0.18
    • #659 build(deps-dev): bump @​types/jest from 27.4.0 to 27.4.1
    • #667 build(deps): bump actions/checkout from 2 to 3
    • #673 build(deps): bump node-fetch from 3.2.0 to 3.2.3
    • #683 build(deps): bump minimist from 1.2.5 to 1.2.6
    • #685 build(deps): bump @​actions/github from 5.0.0 to 5.0.1
    • #681 build(deps-dev): bump @​types/node from 17.0.18 to 17.0.23
    • #682 build(deps-dev): bump typescript from 4.5.5 to 4.6.3
    • #676 build(deps): bump @​actions/exec from 1.1.0 to 1.1.1
    • #675 build(deps): bump openpgp from 5.1.0 to 5.2.1

    2.1.0

    Features

    • #515 Allow specifying version of Codecov uploader

    Dependencies

    • #499 build(deps-dev): bump @​vercel/ncc from 0.29.0 to 0.30.0
    • #508 build(deps): bump openpgp from 5.0.0-5 to 5.0.0
    • #514 build(deps-dev): bump @​types/node from 16.6.0 to 16.9.0

    2.0.3

    Fixes

    • #464 Fix wrong link in the readme
    • #485 fix: Add override OS and linux default to platform

    Dependencies

    • #447 build(deps): bump openpgp from 5.0.0-4 to 5.0.0-5

    ... (truncated)

    Commits
    • 81cd2dc Merge pull request #699 from codecov/feat-xcode
    • a03184e feat: add xcode support
    • 6a6a9ae Merge pull request #694 from codecov/dependabot/npm_and_yarn/vercel/ncc-0.33.4
    • 92a872a Merge pull request #696 from codecov/dependabot/npm_and_yarn/types/node-17.0.25
    • 43a9c18 Merge pull request #698 from codecov/dependabot/npm_and_yarn/jest-junit-13.2.0
    • 13ce822 Merge pull request #690 from codecov/ci-v3
    • 4d6dbaa build(deps-dev): bump jest-junit from 13.0.0 to 13.2.0
    • 98f0f19 build(deps-dev): bump @​types/node from 17.0.23 to 17.0.25
    • d3021d9 build(deps-dev): bump @​vercel/ncc from 0.33.3 to 0.33.4
    • 2c83f35 Update makefile to v3
    • Additional commits viewable in compare view

    Dependabot compatibility score

    Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


    Dependabot commands and options

    You can trigger Dependabot actions by commenting on this PR:

    • @dependabot rebase will rebase this PR
    • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
    • @dependabot merge will merge this PR after your CI passes on it
    • @dependabot squash and merge will squash and merge this PR after your CI passes on it
    • @dependabot cancel merge will cancel a previously requested merge and block automerging
    • @dependabot reopen will reopen this PR if it is closed
    • @dependabot close will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
    • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
    dependencies github_actions 
    opened by dependabot[bot] 0
  • Test expiration

    Test expiration

    I don't think expiration works as intended (or at least as I would expect it).

    The response keys are correctly expired, but nothing is ever removed from the idempotency keys set. This has two consequences:

    • once a response key expires, requests using the same idempotency key will return 409 forever
    • the idempotency keys set grows without bound

    Instead, I would expect expiration to also apply to the idempotency keys set. Once the expiration window has passed, a new request with the same idempotency key should issue a new request.

    This commit only introduces the tests without implementing a solution. For MemoryBackend, I'd propose storing the expiry alongside the idempotency key. For RedisBackend, I'd propose something along the lines of https://github.com/redis/redis/issues/135#issuecomment-2361996 using a sorted set to purge expired idempotency keys. But I wanted to run the change by you before taking action on either approach.

    opened by jmsanders 0
Releases(v0.2.0)
  • v0.2.0(Aug 31, 2022)

    Features and breaking changes

    • Added the option of specifying which HTTP methods to cache. This opens up for making other methods than PATCH and POST idempotent if opted-into (https://github.com/snok/asgi-idempotency-header/pull/6)
    • Removed aioredis for redis. The redis library has absorbed the relevant async code from aioredis in v4.2.

    Improvements and other changes

    • Moved repository to the snok org.
    • Added Python 3.11 to the test matrix
    • Dropped Python 3.7 from test matrix, since it's not currently supported
    Source code(tar.gz)
    Source code(zip)
  • v0.2.0-rc.1(Aug 31, 2022)

  • v0.1.1(Oct 17, 2021)

Owner
Sondre Lillebø Gundersen
Sondre Lillebø Gundersen
Support for Apollo's Automatic Persisted Queries in Strawberry GraphQL 🍓

strawberry-apollo-apq Supporting Apollo's automatic persisted queries in Strawberry GraphQL 🍓 Notes Don't use this for production yet, unless you kno

Bas 3 May 17, 2022
Enable idempotent operations in POST and PATCH endpoints

Idempotency Header ASGI Middleware A middleware for making POST and PATCH endpoints idempotent. The purpose of the middleware is to guarantee that exe

Sondre Lillebø Gundersen 12 Dec 28, 2022
GraphiQL & the GraphQL LSP Reference Ecosystem for building browser & IDE tools.

Black Lives Matter 🖤 GraphQL IDE Monorepo Security Notice: All versions of graphiql 1.4.7 are vulnerable to an XSS attack in cases where the GraphQ

GraphQL 14.5k Jan 08, 2023
An unofficial Blender add-on for Autodesk's Arnold render engine.

Arnold for Blender Arnold for Blender (or BtoA) provides a bridge to the Arnold renderer from within Blender's standard interface. BtoA is an unoffici

Luna Digital, Ltd. 89 Dec 28, 2022
Lightning fast and portable programming language!

Photon Documentation in English Lightning fast and portable programming language! What is Photon? Photon is a programming language aimed at filling th

William 58 Dec 27, 2022
ASGI support for the Tartiflette GraphQL engine

tartiflette-asgi is a wrapper that provides ASGI support for the Tartiflette Python GraphQL engine. It is ideal for serving a GraphQL API over HTTP, o

tartiflette 99 Dec 27, 2022
Burp Suite extension to log GraphQL operations as a comment

Burp GraphQL Logger A very simple, straightforward extension that logs GraphQL operations as a comment in the Proxy view. To enable the highlight, unc

22 Jul 02, 2022
GraphQL Engine built with Python 3.6+ / asyncio

Tartiflette is a GraphQL Server implementation built with Python 3.6+. Summary Motivation Status Usage Installation Installation dependencies Tartifle

tartiflette 839 Dec 31, 2022
Authorization middleware for GraphQL

GraphQL-Authz is a Python3.6+ port of GraphQL-Authz, the node.js implementation for the Casbin authorization middleware.

2 Oct 24, 2022
Getting the ip of a fivem server with a cfx.re link

Dark Utilities - FIVEM-IP-RESOLVER Our Website https://omega-project.cz/ ! Install the app on the server Inplex-sys 12 Oct 25, 2022

A plug and play GraphQL API for Wagtail, powered by Strawberry 🍓

Strawberry Wagtail 🐦 A plug and play GraphQL API for Wagtail, powered by Strawberry 🍓 ⚠️ Strawberry wagtail is currently experimental, please report

Patrick Arminio 27 Nov 27, 2022
Lavrigon - A Python Webservice to check the status of any given local service via a REST call

lavrigon A Python Webservice to check the status of any given local service via

3 Jan 02, 2022
Blazing fast GraphQL endpoints finder using subdomain enumeration, scripts analysis and bruteforce.

Graphinder Graphinder is a tool that extracts all GraphQL endpoints from a given domain. Run with docker docker run -it -v $(pwd):/usr/bin/graphinder

Escape 76 Dec 28, 2022
A python graphql api, which serves ECB currency rates from last 90 days.

Exchange Rate Api using GraphQL Get Code git pull https://github.com/alaturqua/exchangerate-graphql.git Create .env file with following content and s

Isa 1 Nov 04, 2021
tartiflette-aiohttp is a wrapper of aiohttp which includes the Tartiflette GraphQL Engine, do not hesitate to take a look of the Tartiflette project.

tartiflette-aiohttp is a wrapper of aiohttp which includes the Tartiflette GraphQL Engine. You can take a look at the Tartiflette API documentation. U

tartiflette 60 Nov 08, 2022
Pygitstats - a package that allows you to use the GitHub GraphQL API with ease in your Python programs

Pygitstats - a package that allows you to use the GitHub GraphQL API with ease in your Python programs

Dillon Barnes 4 Mar 29, 2022
Graphene MongoEngine integration

Graphene-Mongo A Mongoengine integration for Graphene. Installation For installing graphene-mongo, just run this command in your shell pip install gra

GraphQL Python 261 Dec 31, 2022
GraphQL framework for Python

Graphene 💬 Join the community on Slack We are looking for contributors! Please check the ROADMAP to see how you can help ❤️ The below readme is the d

GraphQL Python 7.5k Jan 01, 2023
GraphQL framework for Python

Graphene 💬 Join the community on Slack We are looking for contributors! Please check the ROADMAP to see how you can help ❤️ The below readme is the d

GraphQL Python 7.5k Jan 01, 2023
UltraGraphQL - a GraphQL interface for querying and modifying RDF data on the Web.

UltraGraphQL - cloned from https://git.rwth-aachen.de/i5/ultragraphql Updated or extended files: build.gradle: updated maven to use maven {url "https:

DrSnowbird 1 Jan 07, 2023