-
Notifications
You must be signed in to change notification settings - Fork 0
/
middleware.go
143 lines (111 loc) · 3.53 KB
/
middleware.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
package idemgotent
import (
"context"
"net/http"
"time"
"github.com/ezraisw/idemgotent/internal/response"
"github.com/ezraisw/wracha"
"github.com/ezraisw/wracha/adapter"
"github.com/ezraisw/wracha/codec/msgpack"
"github.com/ezraisw/wracha/logger"
)
type (
idempotencyMiddleware struct {
name string
// Required options.
adapter adapter.Adapter
logger logger.Logger
// Optional options.
ttl time.Duration
keySource KeySource
clientErrHandler ErrHandler
serverErrHandler ErrHandler
responder Responder
// Initialized.
actor wracha.Actor[serializableResponse]
}
)
const (
ActorNamePrefix = "idemgotent-"
HeaderIdempotencyKey = "Idempotency-Key"
)
var (
defaultOptions = []MiddlewareOption{
WithTTL(wracha.TTLDefault),
WithKeySource(HeaderKeySource(HeaderIdempotencyKey)),
WithClientErrHandler(JSONErrHandler(http.StatusBadRequest)),
WithServerErrHandler(JSONErrHandler(http.StatusInternalServerError)),
WithResponder(CachedResponder(0, "*")),
}
)
func Middleware(name string, options ...MiddlewareOption) func(http.Handler) http.Handler {
m := idempotencyMiddleware{name: name}
m.applyOptions(defaultOptions)
m.applyOptions(options)
m.initActor()
return m.middleware
}
func (m *idempotencyMiddleware) applyOptions(options []MiddlewareOption) {
for _, option := range options {
option(m)
}
}
func (m *idempotencyMiddleware) initActor() {
actorOptions := wracha.ActorOptions{
Adapter: m.adapter,
Logger: m.logger,
// There should be no necessity to override this.
Codec: msgpack.NewCodec(),
}
m.actor = wracha.NewActor[serializableResponse](ActorNamePrefix+m.name, actorOptions).
SetTTL(m.ttl)
}
func (m idempotencyMiddleware) middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Obtain the key from the request.
m.logger.Debug("obtaining key")
key, err := m.keySource(r)
if err != nil {
m.logger.Debug("error while obtaining key", err)
m.clientErrHandler(err, w, r)
return
}
// Do not attempt to cache with empty keys. Skip to the next in chain.
if key == "" {
m.logger.Debug("request has no key")
next.ServeHTTP(w, r)
return
}
// Will be false if the action is executed.
fromCache := true
resp, err := m.actor.Do(
r.Context(),
wracha.KeyableStr(key),
// Will be executed only once for each key.
m.makeAction(func(nw http.ResponseWriter) { fromCache = false; next.ServeHTTP(nw, r) }),
)
if err != nil {
m.logger.Debug("error returned by actor", err)
m.serverErrHandler(err, w, r)
return
}
m.logger.Debug("responding from cache", fromCache)
m.responder.Respond(w, r, CacheResult{FromCache: fromCache, Response: resp})
})
}
func (m idempotencyMiddleware) makeAction(nextWriteTo func(http.ResponseWriter)) wracha.ActionFunc[serializableResponse] {
return func(ctx context.Context) (wracha.ActionResult[serializableResponse], error) {
// Capture the response by substituting http.ResponseWriter with a custom one.
bw := response.NewBufferedResponseWriter()
nextWriteTo(bw)
return wracha.ActionResult[serializableResponse]{Cache: true, Value: m.makeSerializableResponse(bw)}, nil
}
}
func (m idempotencyMiddleware) makeSerializableResponse(bw *response.BufferedResponseWriter) serializableResponse {
var resp serializableResponse
// To reduce cache size.
resp.setStatusCode(bw.StatusCode(), m.responder.CacheStatusCode())
resp.setHeader(bw.Header(), m.responder.CacheHeader())
resp.setBody(bw.Body(), m.responder.CacheBody())
return resp
}