Skip to content

Commit 2f7f5d4

Browse files
committed
backend: cmd: Refactor CacheMiddleWare by extracting handler
Extract the request handling logic from CacheMiddleWare into a separate handleCacheRequest function to reduce cognitive complexity and function length. This removes the need for the nolint directive and makes the code more maintainable and testable. Add documentation comment explaining why ErrHandled is checked and why we return early when it's encountered. This clarifies that ErrHandled is a sentinel error indicating the request was already fully processed during cache invalidation, not an actual error. Change ErrHandled from fmt.Errorf to errors.New to align with Go best practices for sentinel errors. Sentinel errors should be created with errors.New rather than fmt.Errorf to enable proper error comparison with errors.Is. Add missing return statement after handleError when LoadFromCache fails to prevent double writing HTTP responses. This ensures the middleware doesn't continue processing after an error has been written to the client.
1 parent b7546cc commit 2f7f5d4

File tree

3 files changed

+81
-65
lines changed

3 files changed

+81
-65
lines changed

backend/cmd/headlamp_test.go

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1684,7 +1684,9 @@ func newRealK8sHeadlampConfig(t *testing.T) (*HeadlampConfig, string) {
16841684

16851685
if clusterName == "" {
16861686
clusters := (&HeadlampConfig{
1687-
HeadlampCFG: &headlampconfig.HeadlampCFG{KubeConfigStore: kubeConfigStore},
1687+
HeadlampConfig: &headlampconfig.HeadlampConfig{
1688+
HeadlampCFG: &headlampconfig.HeadlampCFG{KubeConfigStore: kubeConfigStore},
1689+
},
16881690
}).getClusters()
16891691
for _, c := range clusters {
16901692
if c.Error == "" {
@@ -1699,17 +1701,19 @@ func newRealK8sHeadlampConfig(t *testing.T) (*HeadlampConfig, string) {
16991701
}
17001702

17011703
c := &HeadlampConfig{
1702-
HeadlampCFG: &headlampconfig.HeadlampCFG{
1703-
UseInCluster: false,
1704-
KubeConfigPath: kubeConfigPath,
1705-
KubeConfigStore: kubeConfigStore,
1706-
CacheEnabled: true,
1707-
PluginDir: pluginDir,
1708-
UserPluginDir: userPluginDir,
1704+
HeadlampConfig: &headlampconfig.HeadlampConfig{
1705+
HeadlampCFG: &headlampconfig.HeadlampCFG{
1706+
UseInCluster: false,
1707+
KubeConfigPath: kubeConfigPath,
1708+
KubeConfigStore: kubeConfigStore,
1709+
CacheEnabled: true,
1710+
PluginDir: pluginDir,
1711+
UserPluginDir: userPluginDir,
1712+
},
1713+
Cache: cache.New[interface{}](),
1714+
TelemetryConfig: GetDefaultTestTelemetryConfig(),
1715+
TelemetryHandler: &telemetry.RequestHandler{},
17091716
},
1710-
cache: cache.New[interface{}](),
1711-
telemetryConfig: GetDefaultTestTelemetryConfig(),
1712-
telemetryHandler: &telemetry.RequestHandler{},
17131717
}
17141718

17151719
return c, clusterName

backend/cmd/server.go

Lines changed: 64 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -183,74 +183,85 @@ func GetContextKeyAndKContext(w http.ResponseWriter,
183183
return ctx, span, contextKey, kContext, nil
184184
}
185185

186-
// CacheMiddleWare is Middleware for Caching purpose. It involves generating key for a request,
187-
// authorizing user , store resource data in cache and returns data if key is present.
188-
//
189-
//nolint:gocognit,funlen
190-
func CacheMiddleWare(c *HeadlampConfig) mux.MiddlewareFunc {
191-
return func(next http.Handler) http.Handler {
192-
if !c.CacheEnabled {
193-
return next
194-
}
186+
// handleCacheRequest processes a request with caching logic.
187+
func handleCacheRequest(c *HeadlampConfig, next http.Handler, w http.ResponseWriter, r *http.Request) {
188+
if k8cache.SkipWebSocket(r, next, w) {
189+
return
190+
}
195191

196-
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
197-
if k8cache.SkipWebSocket(r, next, w) {
198-
return
199-
}
192+
ctx, span, contextKey, kContext, err := GetContextKeyAndKContext(w, r, c)
193+
if err != nil {
194+
return
195+
}
200196

201-
ctx, span, contextKey, kContext, err := GetContextKeyAndKContext(w, r, c)
202-
if err != nil {
203-
return
204-
}
197+
if err := k8cache.HandleNonGETCacheInvalidation(k8sResponseCache, w, r, next, contextKey); err != nil {
198+
// ErrHandled is a sentinel error indicating the request was fully
199+
// processed during cache invalidation. For non-GET requests
200+
// (POST/PUT/DELETE), HandleNonGETCacheInvalidation invalidates the
201+
// cache, makes a fresh request to K8s, stores the response, and
202+
// writes the response to the client. When ErrHandled is returned,
203+
// the request has already been handled and we must return early to
204+
// avoid processing the request again or writing duplicate responses.
205+
if errors.Is(err, k8cache.ErrHandled) {
206+
return
207+
}
208+
209+
c.handleError(w, ctx, span, err, "error while invalidating keys", http.StatusInternalServerError)
205210

206-
if err := k8cache.HandleNonGETCacheInvalidation(k8sResponseCache, w, r, next, contextKey); err != nil {
207-
if errors.Is(err, k8cache.ErrHandled) {
208-
return
209-
}
211+
return
212+
}
210213

211-
c.handleError(w, ctx, span, err, "error while invalidating keys", http.StatusInternalServerError)
214+
rcw := k8cache.NewResponseCapture(w)
212215

213-
return
214-
}
216+
key, err := k8cache.GenerateKey(r.URL, contextKey)
217+
if err != nil {
218+
c.handleError(w, ctx, span, err, "failed to generate key ", http.StatusBadRequest)
219+
return
220+
}
215221

216-
rcw := k8cache.NewResponseCapture(w)
222+
isAllowed, authErr := k8cache.IsAllowed(kContext, r)
223+
if authErr != nil {
224+
k8cache.ServeFromCacheOrForwardToK8s(k8sResponseCache, isAllowed, next, key, w, r, rcw)
217225

218-
key, err := k8cache.GenerateKey(r.URL, contextKey)
219-
if err != nil {
220-
c.handleError(w, ctx, span, err, "failed to generate key ", http.StatusBadRequest)
221-
return
222-
}
226+
return
227+
} else if !isAllowed && k8cache.IsAuthBypassURL(r.URL.Path) {
228+
_ = k8cache.ReturnAuthErrorResponse(w, r, contextKey)
223229

224-
isAllowed, authErr := k8cache.IsAllowed(kContext, r)
225-
if authErr != nil {
226-
k8cache.ServeFromCacheOrForwardToK8s(k8sResponseCache, isAllowed, next, key, w, r, rcw)
230+
return
231+
}
227232

228-
return
229-
} else if !isAllowed && k8cache.IsAuthBypassURL(r.URL.Path) {
230-
_ = k8cache.ReturnAuthErrorResponse(w, r, contextKey)
233+
served, err := k8cache.LoadFromCache(k8sResponseCache, isAllowed, key, w, r)
234+
if err != nil {
235+
c.handleError(w, ctx, span, errors.New(kContext.Error), "failed to load from cache", http.StatusServiceUnavailable)
236+
return
237+
}
231238

232-
return
233-
}
239+
if served {
240+
c.TelemetryHandler.RecordEvent(span, "Served from cache")
241+
return
242+
}
234243

235-
served, err := k8cache.LoadFromCache(k8sResponseCache, isAllowed, key, w, r)
236-
if err != nil {
237-
c.handleError(w, ctx, span, errors.New(kContext.Error), "failed to load from cache", http.StatusServiceUnavailable)
238-
}
244+
k8cache.CheckForChanges(k8sResponseCache, contextKey, *kContext)
239245

240-
if served {
241-
c.TelemetryHandler.RecordEvent(span, "Served from cache")
242-
return
243-
}
246+
next.ServeHTTP(rcw, r)
244247

245-
k8cache.CheckForChanges(k8sResponseCache, contextKey, *kContext)
248+
err = k8cache.StoreK8sResponseInCache(k8sResponseCache, r.URL, rcw, r, key)
249+
if err != nil {
250+
c.handleError(w, ctx, span, errors.New(kContext.Error), "error while storing into cache", http.StatusBadRequest)
251+
return
252+
}
253+
}
246254

247-
next.ServeHTTP(rcw, r)
255+
// CacheMiddleWare is Middleware for Caching purpose. It involves generating key for a request,
256+
// authorizing user , store resource data in cache and returns data if key is present.
257+
func CacheMiddleWare(c *HeadlampConfig) mux.MiddlewareFunc {
258+
return func(next http.Handler) http.Handler {
259+
if !c.CacheEnabled {
260+
return next
261+
}
248262

249-
err = k8cache.StoreK8sResponseInCache(k8sResponseCache, r.URL, rcw, r, key)
250-
if err != nil {
251-
c.handleError(w, ctx, span, errors.New(kContext.Error), "error while storing into cache", http.StatusBadRequest)
252-
return
253-
}
263+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
264+
handleCacheRequest(c, next, w, r)
254265
})
255266
}
256267
}

backend/pkg/k8cache/cacheInvalidation.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ package k8cache
2121

2222
import (
2323
"context"
24+
"errors"
2425
"fmt"
2526
"net/http"
2627
"net/http/httptest"
@@ -88,7 +89,7 @@ func HandleNonGETCacheInvalidation(k8scache cache.Cache[string], w http.Response
8889
}
8990

9091
// ErrHandled indicates the request was fully handled by cache invalidation; middleware must return.
91-
var ErrHandled = fmt.Errorf("handled by cache invalidation")
92+
var ErrHandled = errors.New("handled by cache invalidation")
9293

9394
// SkipWebSocket skip all the websocket requests coming from the client/ frontend to ensure
9495
// real time data updation in the frontend.

0 commit comments

Comments
 (0)