diff --git a/cover.html b/cover.html
deleted file mode 100644
index e6df120..0000000
--- a/cover.html
+++ /dev/null
@@ -1,422 +0,0 @@
-
-
-
-
-
-
-
package potency
-
-import (
- "crypto/sha256"
- "hash"
- "io"
-)
-
-type bodyIntercept struct {
- source io.ReadCloser
- sha256 hash.Hash
-}
-
-func newBodyIntercept(source io.ReadCloser) *bodyIntercept {
- return &bodyIntercept{
- source: source,
- sha256: sha256.New(),
- }
-}
-
-func (bi *bodyIntercept) Read(p []byte) (int, error) {
- numBytes, err := bi.source.Read(p)
- bi.sha256.Write(p[:numBytes])
-
- return numBytes, err
-}
-
-func (bi *bodyIntercept) Close() error {
- return bi.source.Close()
-}
-
-
-
package potency
-
-import (
- "bytes"
- "crypto/sha256"
- "errors"
- "fmt"
- "io"
- "net/http"
- "strings"
- "sync"
- "time"
-
- "github.com/gopatchy/jsrest"
-)
-
-type Potency struct {
- handler http.Handler
-
- lifetime time.Duration
-
- cache map[string]*savedResult
- cacheOldest *savedResult
- cacheNewest *savedResult
- cacheMu sync.RWMutex
-
- inProgress map[string]bool
- inProgressMu sync.Mutex
-}
-
-type savedResult struct {
- key string
-
- method string
- url string
- requestHeader http.Header
- sha256 []byte
-
- statusCode int
- responseHeader http.Header
- responseBody []byte
-
- added time.Time
- newer *savedResult
-}
-
-var (
- ErrConflict = errors.New("conflict")
- ErrMismatch = errors.New("idempotency mismatch")
- ErrBodyMismatch = fmt.Errorf("request body mismatch: %w", ErrMismatch)
- ErrMethodMismatch = fmt.Errorf("HTTP method mismatch: %w", ErrMismatch)
- ErrURLMismatch = fmt.Errorf("URL mismatch: %w", ErrMismatch)
- ErrHeaderMismatch = fmt.Errorf("Header mismatch: %w", ErrMismatch)
- ErrInvalidKey = errors.New("invalid Idempotency-Key")
-
- criticalHeaders = []string{
- "Accept",
- "Authorization",
- }
-)
-
-func NewPotency(handler http.Handler) *Potency {
- return &Potency{
- handler: handler,
- lifetime: 6 * time.Hour,
- cache: map[string]*savedResult{},
- inProgress: map[string]bool{},
- }
-}
-
-func (p *Potency) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- val := r.Header.Get("Idempotency-Key")
- if val == "" {
- p.handler.ServeHTTP(w, r)
- return
- }
-
- err := p.serveHTTP(w, r, val)
- if err != nil {
- jsrest.WriteError(w, err)
- }
-}
-
-func (p *Potency) SetLifetime(lifetime time.Duration) {
- p.cacheMu.Lock()
- defer p.cacheMu.Unlock()
-
- p.lifetime = lifetime
-}
-
-func (p *Potency) NumCached() int {
- p.cacheMu.RLock()
- defer p.cacheMu.RUnlock()
-
- return len(p.cache)
-}
-
-func (p *Potency) serveHTTP(w http.ResponseWriter, r *http.Request, val string) error {
- if len(val) < 2 || !strings.HasPrefix(val, `"`) || !strings.HasSuffix(val, `"`) {
- return jsrest.Errorf(jsrest.ErrBadRequest, "%s (%w)", val, ErrInvalidKey)
- }
-
- key := val[1 : len(val)-1]
-
- saved := p.read(key)
-
- if saved != nil {
- if r.Method != saved.method {
- return jsrest.Errorf(jsrest.ErrBadRequest, "%s (%w)", r.Method, ErrMethodMismatch)
- }
-
- if r.URL.String() != saved.url {
- return jsrest.Errorf(jsrest.ErrBadRequest, "%s (%w)", r.URL.String(), ErrURLMismatch)
- }
-
- for _, h := range criticalHeaders {
- if saved.requestHeader.Get(h) != r.Header.Get(h) {
- return jsrest.Errorf(jsrest.ErrBadRequest, "%s: %s (%w)", h, r.Header.Get(h), ErrHeaderMismatch)
- }
- }
-
- h := sha256.New()
-
- _, err := io.Copy(h, r.Body)
- if err != nil {
- return jsrest.Errorf(jsrest.ErrBadRequest, "hash request body failed (%w)", err)
- }
-
- sha256 := h.Sum(nil)
- if !bytes.Equal(sha256, saved.sha256) {
- return jsrest.Errorf(jsrest.ErrBadRequest, "%s vs %s (%w)", sha256, saved.sha256, ErrBodyMismatch)
- }
-
- for key, vals := range saved.responseHeader {
- w.Header().Set(key, vals[0])
- }
-
- w.WriteHeader(saved.statusCode)
- _, _ = w.Write(saved.responseBody)
-
- return nil
- }
-
- // Store miss, proceed to normal execution with interception
- err := p.lockKey(key)
- if err != nil {
- return jsrest.Errorf(jsrest.ErrConflict, "%s", key)
- }
-
- defer p.unlockKey(key)
-
- requestHeader := http.Header{}
- for _, h := range criticalHeaders {
- requestHeader.Set(h, r.Header.Get(h))
- }
-
- bi := newBodyIntercept(r.Body)
- r.Body = bi
-
- rwi := newResponseWriterIntercept(w)
- w = rwi
-
- p.handler.ServeHTTP(w, r)
-
- save := &savedResult{
- key: key,
-
- method: r.Method,
- url: r.URL.String(),
- requestHeader: requestHeader,
- sha256: bi.sha256.Sum(nil),
-
- statusCode: rwi.statusCode,
- responseHeader: rwi.Header(),
- responseBody: rwi.buf.Bytes(),
- }
-
- p.write(save)
-
- return nil
-}
-
-func (p *Potency) lockKey(key string) error {
- p.inProgressMu.Lock()
- defer p.inProgressMu.Unlock()
-
- if p.inProgress[key] {
- return ErrConflict
- }
-
- p.inProgress[key] = true
-
- return nil
-}
-
-func (p *Potency) unlockKey(key string) {
- p.inProgressMu.Lock()
- defer p.inProgressMu.Unlock()
-
- delete(p.inProgress, key)
-}
-
-func (p *Potency) read(key string) *savedResult {
- p.cacheMu.RLock()
- defer p.cacheMu.RUnlock()
-
- return p.cache[key]
-}
-
-func (p *Potency) write(sr *savedResult) {
- p.cacheMu.Lock()
- defer p.cacheMu.Unlock()
-
- sr.added = time.Now()
-
- p.cache[sr.key] = sr
-
- if p.cacheNewest != nil {
- p.cacheNewest.newer = sr
- }
-
- p.cacheNewest = sr
-
- if p.cacheOldest == nil {
- p.cacheOldest = sr
- }
-
- p.removeExpired()
-}
-
-func (p *Potency) removeExpired() {
- cutoff := time.Now().Add(-1 * p.lifetime)
-
- for iter := p.cacheOldest; iter != nil && iter.added.Before(cutoff); iter = iter.newer {
- delete(p.cache, iter.key)
- p.cacheOldest = iter
- }
-}
-
-
-
package potency
-
-import (
- "bytes"
- "net/http"
-)
-
-type responseWriterIntercept struct {
- dest http.ResponseWriter
- buf bytes.Buffer
- statusCode int
-}
-
-func newResponseWriterIntercept(dest http.ResponseWriter) *responseWriterIntercept {
- return &responseWriterIntercept{
- dest: dest,
- buf: bytes.Buffer{},
- statusCode: http.StatusOK,
- }
-}
-
-func (rwi *responseWriterIntercept) Header() http.Header {
- return rwi.dest.Header()
-}
-
-func (rwi *responseWriterIntercept) Write(data []byte) (int, error) {
- rwi.buf.Write(data)
- return rwi.dest.Write(data)
-}
-
-func (rwi *responseWriterIntercept) WriteHeader(statusCode int) {
- rwi.statusCode = statusCode
- rwi.dest.WriteHeader(statusCode)
-}
-
-
-
-
-
-
diff --git a/cover.out b/cover.out
deleted file mode 100644
index 59502da..0000000
--- a/cover.out
+++ /dev/null
@@ -1,49 +0,0 @@
-mode: atomic
-github.com/gopatchy/potency/bodyintercept.go:14.60,19.2 1 6
-github.com/gopatchy/potency/bodyintercept.go:21.54,26.2 3 6
-github.com/gopatchy/potency/bodyintercept.go:28.40,30.2 1 0
-github.com/gopatchy/potency/potency.go:62.48,69.2 1 3
-github.com/gopatchy/potency/potency.go:71.69,73.15 2 14
-github.com/gopatchy/potency/potency.go:73.15,76.3 2 0
-github.com/gopatchy/potency/potency.go:78.2,79.16 2 14
-github.com/gopatchy/potency/potency.go:79.16,81.3 1 5
-github.com/gopatchy/potency/potency.go:84.55,89.2 3 1
-github.com/gopatchy/potency/potency.go:91.35,96.2 3 4
-github.com/gopatchy/potency/potency.go:98.87,99.82 1 14
-github.com/gopatchy/potency/potency.go:99.82,101.3 1 0
-github.com/gopatchy/potency/potency.go:103.2,107.18 3 14
-github.com/gopatchy/potency/potency.go:107.18,108.31 1 8
-github.com/gopatchy/potency/potency.go:108.31,110.4 1 1
-github.com/gopatchy/potency/potency.go:112.3,112.34 1 7
-github.com/gopatchy/potency/potency.go:112.34,114.4 1 1
-github.com/gopatchy/potency/potency.go:116.3,116.37 1 6
-github.com/gopatchy/potency/potency.go:116.37,117.53 1 11
-github.com/gopatchy/potency/potency.go:117.53,119.5 1 2
-github.com/gopatchy/potency/potency.go:122.3,125.17 3 4
-github.com/gopatchy/potency/potency.go:125.17,127.4 1 0
-github.com/gopatchy/potency/potency.go:129.3,130.41 2 4
-github.com/gopatchy/potency/potency.go:130.41,132.4 1 1
-github.com/gopatchy/potency/potency.go:134.3,134.47 1 3
-github.com/gopatchy/potency/potency.go:134.47,136.4 1 3
-github.com/gopatchy/potency/potency.go:138.3,141.13 3 3
-github.com/gopatchy/potency/potency.go:145.2,146.16 2 6
-github.com/gopatchy/potency/potency.go:146.16,148.3 1 0
-github.com/gopatchy/potency/potency.go:150.2,153.36 3 6
-github.com/gopatchy/potency/potency.go:153.36,155.3 1 12
-github.com/gopatchy/potency/potency.go:157.2,180.12 8 6
-github.com/gopatchy/potency/potency.go:183.45,187.23 3 6
-github.com/gopatchy/potency/potency.go:187.23,189.3 1 0
-github.com/gopatchy/potency/potency.go:191.2,193.12 2 6
-github.com/gopatchy/potency/potency.go:196.41,201.2 3 6
-github.com/gopatchy/potency/potency.go:203.49,208.2 3 14
-github.com/gopatchy/potency/potency.go:210.42,218.26 5 6
-github.com/gopatchy/potency/potency.go:218.26,220.3 1 3
-github.com/gopatchy/potency/potency.go:222.2,224.26 2 6
-github.com/gopatchy/potency/potency.go:224.26,226.3 1 3
-github.com/gopatchy/potency/potency.go:228.2,228.19 1 6
-github.com/gopatchy/potency/potency.go:231.35,234.89 2 6
-github.com/gopatchy/potency/potency.go:234.89,237.3 2 2
-github.com/gopatchy/potency/responsewriterintercept.go:14.84,20.2 1 6
-github.com/gopatchy/potency/responsewriterintercept.go:22.58,24.2 1 12
-github.com/gopatchy/potency/responsewriterintercept.go:26.69,29.2 2 6
-github.com/gopatchy/potency/responsewriterintercept.go:31.65,34.2 2 0
diff --git a/go.mod b/go.mod
index 7bc3f08..4828448 100644
--- a/go.mod
+++ b/go.mod
@@ -5,14 +5,14 @@ go 1.19
require (
github.com/dchest/uniuri v1.2.0
github.com/go-resty/resty/v2 v2.7.0
- github.com/gopatchy/jsrest v0.0.0-20230516044821-deb630cd744b
+ github.com/gopatchy/jsrest v0.0.0-20230516044950-1c6d15dad16a
github.com/stretchr/testify v1.8.2
go.uber.org/goleak v1.2.1
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
- github.com/gopatchy/metadata v0.0.0-20230516041300-fc49e5f775fe // indirect
+ github.com/gopatchy/metadata v0.0.0-20230516044939-eed23a0903d5 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/vfaronov/httpheader v0.1.0 // indirect
golang.org/x/net v0.10.0 // indirect
diff --git a/go.sum b/go.sum
index eec09ae..7b43565 100644
--- a/go.sum
+++ b/go.sum
@@ -9,10 +9,14 @@ github.com/gopatchy/jsrest v0.0.0-20230511133808-abcf8276d1ad h1:UKIRgnQNkQ7qkMD
github.com/gopatchy/jsrest v0.0.0-20230511133808-abcf8276d1ad/go.mod h1:77Kp7hFjygVlnDH48qO917da0gyKNR2XG8mZtaAEL9w=
github.com/gopatchy/jsrest v0.0.0-20230516044821-deb630cd744b h1:fnoLhs0kk7rc/hdk1vn8lzJ9uiq1ANr3klSUwbze1ys=
github.com/gopatchy/jsrest v0.0.0-20230516044821-deb630cd744b/go.mod h1:MQBtK0M/Uota4CpW5NbsjDqWuoQyYjS70PJSxWf3nLE=
+github.com/gopatchy/jsrest v0.0.0-20230516044950-1c6d15dad16a h1:ehV4YITvldTIuTMAq7kk0RcrBE7m3WQKFzFl/jOqh7w=
+github.com/gopatchy/jsrest v0.0.0-20230516044950-1c6d15dad16a/go.mod h1:TVfwj+gk7iCGJRrVYR+0ovXhOc90UXPrBCOGPLE8Fsw=
github.com/gopatchy/metadata v0.0.0-20230424223338-33e58fee42bf h1:HKCbhVEpC3++ydeapSJN2DGs9KGWMvOLpcZrwKkrXQs=
github.com/gopatchy/metadata v0.0.0-20230424223338-33e58fee42bf/go.mod h1:VgD33raUShjDePCDBo55aj+eSXFtUEpMzs+Ie39g2zo=
github.com/gopatchy/metadata v0.0.0-20230516041300-fc49e5f775fe h1:xPnlis/qCAYoxHx9tow1P4pO17c8JH/Hs/lHDmZej/Q=
github.com/gopatchy/metadata v0.0.0-20230516041300-fc49e5f775fe/go.mod h1:VgD33raUShjDePCDBo55aj+eSXFtUEpMzs+Ie39g2zo=
+github.com/gopatchy/metadata v0.0.0-20230516044939-eed23a0903d5 h1:b66b4DOGTqDuw4hbxHSp0WbhXr/xAMaiFkU6iCi4nDg=
+github.com/gopatchy/metadata v0.0.0-20230516044939-eed23a0903d5/go.mod h1:VgD33raUShjDePCDBo55aj+eSXFtUEpMzs+Ie39g2zo=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=