Skip to content

ezraisw/idemgotent

Repository files navigation

Idemgotent

Middleware for providing idempotency for APIs.

Uses WraCha as its base. Safe for multi-threaded/multi-instance use.

Installation

Simply run the following command to install:

go get github.com/ezraisw/idemgotent

Usage

Initialization

package main

import (
    "net/http"

    "github.com/go-chi/chi"
    "github.com/go-redis/redis/v8"
    "github.com/ezraisw/idemgotent"
    "github.com/ezraisw/wracha/adapter/goredis"
    "github.com/ezraisw/wracha/logger/std"
)

func main() {
    // ... your router (example with go-chi)
    r := chi.NewRouter()

    // ... your redis client
    client := redis.NewClient(&redis.Options{
        // ...
    })

    middleware := idemgotent.Middleware("/your/path/to/route",
        idemgotent.WithAdapter(goredis.NewAdapter(client)),
        idemgotent.WithLogger(std.NewLogger()),
    )

    // ... (example 1 with go-chi)
    r.Group(func(r chi.Router) {
        r.Use(middleware)
        r.Post("/your/path/to/route", myHandler)
    })

    // ... (example 2 with go-chi)
    r.With(middleware).Post("/your/path/to/route", myHandler)
}

func myHandler(w http.ResponseWriter, r *http.Request) {
    // ...
}

Extra Configuration

By design, this library is meant to be configurable and modular at certain parts.

Determining Key

By default, the library uses the value of the header Idempotency-Key for determining idempotency. You can configure this by passing

idemgotent.WithKeySource(idemgotent.HeaderKeySource("Custom-Idempotent-Key"))

to the options argument.

Alternatively, you can also define your own way to obtain the key by satisfying the KeySource function type. Be careful when obtaining idempotency keys from body as you might have to do some workarounds to allow multiple reads of the request body.

type KeySource func(r *http.Request) (string, error)
func JSONKeySource(name string) idemgotent.KeySource {
    return func(r *http.Request) (string, error) {
        // ... unmarshal and read JSON.
    }
}

Responding to Clients

By default, the library responds with the previously cached response along with its status code and headers. This is override-able by passing

idemgotent.WithResponder(idemgotent.CachedResponder(http.StatusNotModified, "Content-Type"))

to the options argument.

You can also implement your own responder.

type Responder interface {
    CacheStatusCode() bool
    CacheHeader() bool
    CacheBody() bool
    Respond(http.ResponseWriter, *http.Request, CacheResult)
}
type conflictResponder struct {
}

func (conflictResponder) CacheStatusCode() bool {
    return false
}

func (conflictResponder) CacheHeader() bool {
    return false
}

func (conflictResponder) CacheBody() bool {
    return false
}

func (rp conflictResponder) Respond(w http.ResponseWriter, r *http.Request, cr CacheResult) {
    if cr.FromCache {
        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(http.StatusConflict)
        w.Write([]byte("{\"message\": \"idempotency violation\"}"))
        return
    }

    cr.CopyHeaderTo(w, nil)
    w.WriteHeader(cr.Response.GetStatusCode())
    w.Write(cr.Response.GetBody())
}

About

⏱️ Idempotency middleware for HTTP APIs

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages