feat(cache): refresh request meta on cache hits
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
00a59947b4
commit
37b7fd21ae
2 changed files with 133 additions and 3 deletions
60
cache.go
60
cache.go
|
|
@ -5,8 +5,11 @@ package api
|
|||
import (
|
||||
"bytes"
|
||||
"container/list"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"maps"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
|
|
@ -124,10 +127,18 @@ func cacheMiddleware(store *cacheStore, ttl time.Duration) gin.HandlerFunc {
|
|||
|
||||
// Serve from cache if a valid entry exists.
|
||||
if entry := store.get(key); entry != nil {
|
||||
body := entry.body
|
||||
if meta := GetRequestMeta(c); meta != nil {
|
||||
body = refreshCachedResponseMeta(entry.body, meta)
|
||||
}
|
||||
|
||||
for k, vals := range entry.headers {
|
||||
if http.CanonicalHeaderKey(k) == "X-Request-ID" {
|
||||
continue
|
||||
}
|
||||
if http.CanonicalHeaderKey(k) == "Content-Length" {
|
||||
continue
|
||||
}
|
||||
for _, v := range vals {
|
||||
c.Writer.Header().Set(k, v)
|
||||
}
|
||||
|
|
@ -138,8 +149,9 @@ func cacheMiddleware(store *cacheStore, ttl time.Duration) gin.HandlerFunc {
|
|||
c.Writer.Header().Set("X-Request-ID", requestID)
|
||||
}
|
||||
c.Writer.Header().Set("X-Cache", "HIT")
|
||||
c.Writer.Header().Set("Content-Length", strconv.Itoa(len(body)))
|
||||
c.Writer.WriteHeader(entry.status)
|
||||
_, _ = c.Writer.Write(entry.body)
|
||||
_, _ = c.Writer.Write(body)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
|
@ -167,3 +179,49 @@ func cacheMiddleware(store *cacheStore, ttl time.Duration) gin.HandlerFunc {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// refreshCachedResponseMeta updates the meta envelope in a cached JSON body so
|
||||
// request-scoped metadata reflects the current request instead of the cache fill.
|
||||
// Non-JSON bodies, malformed JSON, and responses without a top-level object are
|
||||
// returned unchanged.
|
||||
func refreshCachedResponseMeta(body []byte, meta *Meta) []byte {
|
||||
if meta == nil {
|
||||
return body
|
||||
}
|
||||
|
||||
var payload any
|
||||
dec := json.NewDecoder(bytes.NewReader(body))
|
||||
dec.UseNumber()
|
||||
if err := dec.Decode(&payload); err != nil {
|
||||
return body
|
||||
}
|
||||
var extra any
|
||||
if err := dec.Decode(&extra); err != io.EOF {
|
||||
return body
|
||||
}
|
||||
|
||||
obj, ok := payload.(map[string]any)
|
||||
if !ok {
|
||||
return body
|
||||
}
|
||||
|
||||
current := map[string]any{}
|
||||
if existing, ok := obj["meta"].(map[string]any); ok {
|
||||
current = existing
|
||||
}
|
||||
|
||||
if meta.RequestID != "" {
|
||||
current["request_id"] = meta.RequestID
|
||||
}
|
||||
if meta.Duration != "" {
|
||||
current["duration"] = meta.Duration
|
||||
}
|
||||
|
||||
obj["meta"] = current
|
||||
|
||||
updated, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return body
|
||||
}
|
||||
return updated
|
||||
}
|
||||
|
|
|
|||
|
|
@ -250,8 +250,80 @@ func TestWithCache_Good_PreservesCurrentRequestIDOnHit(t *testing.T) {
|
|||
if got := w2.Header().Get("X-Cache"); got != "HIT" {
|
||||
t.Fatalf("expected X-Cache=HIT, got %q", got)
|
||||
}
|
||||
if body1, body2 := w1.Body.String(), w2.Body.String(); body1 != body2 {
|
||||
t.Fatalf("expected cached body %q, got %q", body1, body2)
|
||||
|
||||
var resp2 api.Response[string]
|
||||
if err := json.Unmarshal(w2.Body.Bytes(), &resp2); err != nil {
|
||||
t.Fatalf("unmarshal error: %v", err)
|
||||
}
|
||||
if resp2.Data != "call-1" {
|
||||
t.Fatalf("expected cached response data %q, got %q", "call-1", resp2.Data)
|
||||
}
|
||||
if resp2.Meta == nil {
|
||||
t.Fatal("expected cached response meta to be attached")
|
||||
}
|
||||
if resp2.Meta.RequestID != "second-request-id" {
|
||||
t.Fatalf("expected cached response request_id=%q, got %q", "second-request-id", resp2.Meta.RequestID)
|
||||
}
|
||||
if resp2.Meta.Duration == "" {
|
||||
t.Fatal("expected cached response duration to be refreshed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithCache_Good_PreservesCurrentRequestMetaOnHit(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
e, _ := api.New(
|
||||
api.WithRequestID(),
|
||||
api.WithCache(5*time.Second),
|
||||
)
|
||||
e.Register(requestMetaTestGroup{})
|
||||
|
||||
h := e.Handler()
|
||||
|
||||
w1 := httptest.NewRecorder()
|
||||
req1, _ := http.NewRequest(http.MethodGet, "/v1/meta", nil)
|
||||
req1.Header.Set("X-Request-ID", "first-request-id")
|
||||
h.ServeHTTP(w1, req1)
|
||||
if w1.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", w1.Code)
|
||||
}
|
||||
|
||||
var resp1 api.Response[string]
|
||||
if err := json.Unmarshal(w1.Body.Bytes(), &resp1); err != nil {
|
||||
t.Fatalf("unmarshal error: %v", err)
|
||||
}
|
||||
if resp1.Meta == nil {
|
||||
t.Fatal("expected meta on first response")
|
||||
}
|
||||
if resp1.Meta.RequestID != "first-request-id" {
|
||||
t.Fatalf("expected first response request_id=%q, got %q", "first-request-id", resp1.Meta.RequestID)
|
||||
}
|
||||
|
||||
w2 := httptest.NewRecorder()
|
||||
req2, _ := http.NewRequest(http.MethodGet, "/v1/meta", nil)
|
||||
req2.Header.Set("X-Request-ID", "second-request-id")
|
||||
h.ServeHTTP(w2, req2)
|
||||
if w2.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", w2.Code)
|
||||
}
|
||||
|
||||
var resp2 api.Response[string]
|
||||
if err := json.Unmarshal(w2.Body.Bytes(), &resp2); err != nil {
|
||||
t.Fatalf("unmarshal error: %v", err)
|
||||
}
|
||||
if resp2.Meta == nil {
|
||||
t.Fatal("expected meta on cached response")
|
||||
}
|
||||
if resp2.Meta.RequestID != "second-request-id" {
|
||||
t.Fatalf("expected cached response request_id=%q, got %q", "second-request-id", resp2.Meta.RequestID)
|
||||
}
|
||||
if resp2.Meta.Duration == "" {
|
||||
t.Fatal("expected cached response duration to be refreshed")
|
||||
}
|
||||
if resp2.Meta.Page != 1 || resp2.Meta.PerPage != 25 || resp2.Meta.Total != 100 {
|
||||
t.Fatalf("expected pagination metadata to remain intact, got %+v", resp2.Meta)
|
||||
}
|
||||
if got := w2.Header().Get("X-Request-ID"); got != "second-request-id" {
|
||||
t.Fatalf("expected response header X-Request-ID=%q, got %q", "second-request-id", got)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue