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
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
Generate daily updated visualizations of user and repository statistics from the GitHub API using GitHub Actions

Generate daily updated visualizations of user and repository statistics from the GitHub API using GitHub Actions for any combination of private and public repositories - dark mode supported

Adam Ross 15 Dec 31, 2022
A Django GraphQL Starter that uses graphene and graphene_django to interface GraphQL.

Django GraphQL Starter GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data... According to the doc

0101 Solutions 1 Jan 10, 2022
Modular, cohesive, transparent and fast web server template

kingdom-python-server 🐍 Modular, transparent, batteries (half) included, lightning fast web server. Features a functional, isolated business layer wi

T10 20 Feb 08, 2022
Simple GraphQL client for Python 2.7+

python-graphql-client Simple GraphQL client for Python 2.7+ Install pip install graphqlclient Usage from graphqlclient import GraphQLClient client =

Prisma Labs 150 Nov 29, 2022
🔪 Facebook Messenger to email bridge based on reverse engineered auth and GraphQL APIs.

Unzuckify This repository has a small Python application which allows me to receive an email notification when somebody sends me a Facebook message. W

Radon Rosborough 33 Dec 18, 2022
Django GraphQL To Do List Application

Django GraphQL Simple ToDo HOW TO RUN just run the following instructions: python -m venv venv pip install -r requirements.txt source venv/bin/activat

pedram shahsafi 1 Nov 13, 2021
A library to help construct a graphql-py server supporting react-relay

Relay Library for GraphQL Python GraphQL-relay-py is the Relay library for GraphQL-core. It allows the easy creation of Relay-compliant servers using

GraphQL Python 143 Nov 15, 2022
Generate a FullStack Playground using GraphQL and FastAPI 🚀

FastQL - FastAPI GraphQL Playground Generate a FullStack playground using FastAPI and GraphQL and Ariadne 🚀 . This Repository is based on this Articl

OBytes 109 Dec 23, 2022
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
Django registration and authentication with GraphQL.

Django GraphQL Auth Django registration and authentication with GraphQL. Demo About Abstract all the basic logic of handling user accounts out of your

pedrobern 301 Dec 09, 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
Gerenciar a velocidade da internet banda larga

Monitoramento da Velocidade da internet 📶 Status do Projeto: ✔️ (pronto) Tópicos ✍️ Descrição do projeto Funcionalidades Deploy da Aplicação Pré-requ

Bárbara Guerbas de Figueiredo 147 Nov 02, 2022
This is a simple Python that will parse instanceStats GraphQL Query into a CSV

GraphQL Python Labs - by Gabs the CSE Table of Contents About The Project Getting Started Prerequisites Installation and Usage Roadmap Contributing Li

Gabriel (Gabs) Cerioni 1 Oct 27, 2021
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
A real time webchat made in graphql

Graphql Chat. This is a real time webchat made in graphql. Description Welcome to my webchat api, here i put my knowledge in graphql to work. Requirem

Nathan André 1 Jan 03, 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
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
Fastapi strawberry graphql

fastapi-strawberry-graphql Quick and dirty 🍓 python python --version Python 3.10 pip pip install sqlalchemy pip install sqlmodel pip install fastapi

Rodrigo Ney 7 Oct 19, 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