Middleware for providing idempotency for APIs.
Uses WraCha as its base. Safe for multi-threaded/multi-instance use.
Simply run the following command to install:
go get github.com/ezraisw/idemgotent
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) {
// ...
}
By design, this library is meant to be configurable and modular at certain parts.
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.
}
}
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())
}