This repository has been archived by the owner on Nov 22, 2018. It is now read-only.
forked from nicolasazrak/caddy-cache
-
Notifications
You must be signed in to change notification settings - Fork 0
/
handler.go
220 lines (175 loc) · 5.36 KB
/
handler.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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
package cache
import (
"context"
"net/http"
"strings"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
// Handler is the main cache middleware
type Handler struct {
// Handler configuration
Config *Config
// Cache is where entries are stored
Cache *HTTPCache
// Next handler
Next httpserver.Handler
// Handles locking for different URLs
URLLocks *URLLock
}
const (
cacheHit = "hit"
cacheMiss = "miss"
cacheSkip = "skip"
cacheBypass = "bypass"
)
func getKey(r *http.Request) string {
key := r.Method + " " + r.Host + r.URL.Path
q := r.URL.Query().Encode()
if len(q) > 0 {
key += "?" + q
}
return key
}
// NewHandler creates a new Handler using Next middleware
func NewHandler(Next httpserver.Handler, config *Config) *Handler {
return &Handler{
Config: config,
Cache: NewHTTPCache(),
URLLocks: NewURLLock(),
Next: Next,
}
}
/* Responses */
func copyHeaders(from http.Header, to http.Header) {
for k, values := range from {
for _, v := range values {
to.Add(k, v)
}
}
}
func (handler *Handler) addStatusHeaderIfConfigured(w http.ResponseWriter, status string) {
if rec, ok := w.(*httpserver.ResponseRecorder); ok {
rec.Replacer.Set("cache_status", status)
}
if handler.Config.StatusHeader != "" {
w.Header().Add(handler.Config.StatusHeader, status)
}
}
func (handler *Handler) respond(w http.ResponseWriter, entry *HTTPCacheEntry, cacheStatus string) (int, error) {
handler.addStatusHeaderIfConfigured(w, cacheStatus)
copyHeaders(entry.Response.snapHeader, w.Header())
w.WriteHeader(entry.Response.Code)
err := entry.WriteBodyTo(w)
return entry.Response.Code, err
}
/* Handler */
func shouldUseCache(req *http.Request) bool {
// TODO Add more logic like get params, ?nocache=true
if req.Method != "GET" && req.Method != "HEAD" {
// Only cache Get and head request
return false
}
// Range requests still not supported
// It may happen that the previous request for this url has a successful response
// but for another Range. So a special handling is needed
if req.Header.Get("range") != "" {
return false
}
if strings.ToLower(req.Header.Get("Connection")) == "upgrade" && strings.ToLower(req.Header.Get("Upgrade")) == "websocket" {
return false
}
return true
}
func popOrNil(errChan chan error) (err error) {
select {
case err = <-errChan:
default:
}
return
}
func (handler *Handler) fetchUpstream(req *http.Request) (*HTTPCacheEntry, error) {
// Create a new empty response
response := NewResponse()
errChan := make(chan error, 1)
// Do the upstream fetching in background
go func(req *http.Request, response *Response) {
// Create a new context to avoid terminating the Next.ServeHTTP when the original
// request is closed. Otherwise if the original request is cancelled the other requests
// will see a bad response that has the same contents the first request has
updatedReq := req.WithContext(context.Background())
statusCode, upstreamError := handler.Next.ServeHTTP(response, updatedReq)
errChan <- upstreamError
// If status code was not set, this will not replace it
// It will only ensure status code IS send
response.WriteHeader(statusCode)
// Wait the response body to be set.
// If it is private it will be the original http.ResponseWriter
// It is required to wait the body to prevent closing the response
// before the body was set. If that happens the body will
// stay locked waiting the response to be closed
response.WaitBody()
response.Close()
}(req, response)
// Wait headers to de sent
response.WaitHeaders()
// Create a new CacheEntry
return NewHTTPCacheEntry(getKey(req), req, response, handler.Config), popOrNil(errChan)
}
func (handler *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
if !shouldUseCache(r) {
handler.addStatusHeaderIfConfigured(w, cacheBypass)
return handler.Next.ServeHTTP(w, r)
}
lock := handler.URLLocks.Adquire(getKey(r))
// Lookup correct entry
previousEntry, exists := handler.Cache.Get(r)
// First case: CACHE HIT
// The response exists in cache and is public
// It should be served as saved
if exists && previousEntry.isPublic {
lock.Unlock()
return handler.respond(w, previousEntry, cacheHit)
}
// Second case: CACHE SKIP
// The response is in cache but it is not public
// It should NOT be served from cache
// It should be fetched from upstream and check the new headers
// To check if the new response changes to public
if exists && !previousEntry.isPublic {
lock.Unlock()
entry, err := handler.fetchUpstream(r)
if err != nil {
return entry.Response.Code, err
}
// Case when response was private but now is public
if entry.isPublic {
err := entry.setStorage(handler.Config)
if err != nil {
return 500, err
}
handler.Cache.Put(r, entry)
return handler.respond(w, entry, cacheMiss)
}
return handler.respond(w, entry, cacheSkip)
}
// Third case: CACHE MISS
// The response is not in cache
// It should be fetched from upstream and save it in cache
entry, err := handler.fetchUpstream(r)
if err != nil {
lock.Unlock()
return entry.Response.Code, err
}
// Entry is always saved, even if it is not public
// This is to release the URL lock.
if entry.isPublic {
err := entry.setStorage(handler.Config)
if err != nil {
lock.Unlock()
return 500, err
}
}
handler.Cache.Put(r, entry)
lock.Unlock()
return handler.respond(w, entry, cacheMiss)
}