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
GraphQL is a query language and execution engine tied to any backend service.

GraphQL The GraphQL specification is edited in the markdown files found in /spec the latest release of which is published at https://graphql.github.io

GraphQL 14k Jan 1, 2023
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 9, 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 8, 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
Django Project with Rest and Graphql API's

Django-Rest-and-Graphql # 1. Django Project Setup With virtual environment: mkdir {project_name}. To install virtual Environment sudo apt-get install

Shubham Agrawal 5 Nov 22, 2022
GraphQL security auditing script with a focus on performing batch GraphQL queries and mutations

BatchQL BatchQL is a GraphQL security auditing script with a focus on performing batch GraphQL queries and mutations. This script is not complex, and

Assetnote 267 Dec 24, 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
This is a minimal project using graphene with django and user authentication to expose a graphql endpoint.

Welcome This is a minimal project using graphene with django and user authentication to expose a graphql endpoint. Definitely checkout how I have mana

yosef salmalian 1 Nov 18, 2021
🔪 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
This is a graphql api build using ariadne python that serves a graphql-endpoint at port 3002 to perform language translation and identification using deep learning in python pytorch.

Language Translation and Identification this machine/deep learning api that will be served as a graphql-api using ariadne, to perform the following ta

crispengari 2 Dec 30, 2021
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
MGE-GraphQL is a Python library for building GraphQL mutations fast and easily

MGE-GraphQL Introduction MGE-GraphQL is a Python library for building GraphQL mutations fast and easily. Data Validations: A similar data validation w

MGE Software 4 Apr 23, 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
PyTorch implementation of the Transformer in Post-LN (Post-LayerNorm) and Pre-LN (Pre-LayerNorm).

Transformer-PyTorch A PyTorch implementation of the Transformer from the paper Attention is All You Need in both Post-LN (Post-LayerNorm) and Pre-LN (

Jared Wang 22 Feb 27, 2022
DIT is a DTLS MitM proxy implemented in Python 3. It can intercept, manipulate and suppress datagrams between two DTLS endpoints and supports psk-based and certificate-based authentication schemes (RSA + ECC).

DIT - DTLS Interception Tool DIT is a MitM proxy tool to intercept DTLS traffic. It can intercept, manipulate and/or suppress DTLS datagrams between t

null 52 Nov 30, 2022
python3.5+ hubspot client based on hapipy, but modified to use the newer endpoints and non-legacy python

A python wrapper around HubSpot's APIs, for python 3.5+. Built initially around hapipy, but heavily modified. Check out the documentation here! (thank

Jacobi Petrucciani 140 Dec 21, 2022
ASGI middleware for authentication, rate limiting, and building CRUD endpoints.

Piccolo API Utilities for easily exposing Piccolo models as REST endpoints in ASGI apps, such as Starlette and FastAPI. Includes a bunch of useful ASG

null 81 Dec 9, 2022
Django-rest-auth provides a set of REST API endpoints for Authentication and Registration

This app makes it extremely easy to build Django powered SPA's (Single Page App) or Mobile apps exposing all registration and authentication related functionality as CBV's (Class Base View) and REST (JSON)

Tivix 2.4k Dec 29, 2022
A project based example of Data pipelines, ML workflow management, API endpoints and Monitoring.

MLOps template with examples for Data pipelines, ML workflow management, API development and Monitoring.

Utsav 33 Dec 3, 2022
Endpoints is a lightweight REST api framework written in python and used in multiple production systems that handle millions of requests daily.

Endpoints Quickest API builder in the West! Endpoints is a lightweight REST api framework written in python and used in multiple production systems th

Jay Marcyes 30 Mar 5, 2022