reference-api/unified_handler.go (116 lines of code) (raw):

package main import ( "bytes" "fmt" "log" "net/http" "net/http/httptest" "time" lru "github.com/hashicorp/golang-lru/v2" ) type HandlerDeps struct { CommitsHandler http.HandlerFunc ReleasesHandler http.HandlerFunc cache *lru.Cache[string, CachedResponse] config cacheConfiguration } type CachedResponse struct { StatusCode int Body []byte Timestamp int64 // Unix timestamp when stored } type responseRecorder struct { http.ResponseWriter statusCode int body bytes.Buffer } type cacheConfiguration struct { SuccessCacheDuration time.Duration ErrorCacheDuration time.Duration } func (rec *responseRecorder) Write(p []byte) (int, error) { if rec.statusCode == 0 { // Default to 200 if WriteHeader was never called rec.statusCode = http.StatusOK } rec.body.Write(p) return rec.ResponseWriter.Write(p) } func (rec *responseRecorder) WriteHeader(code int) { rec.statusCode = code rec.ResponseWriter.WriteHeader(code) } // UnifiedHandler with in-memory caching func (deps *HandlerDeps) UnifiedHandler(w http.ResponseWriter, r *http.Request) { repo := r.URL.Query().Get("repo") gitRef := r.URL.Query().Get("gitRef") if repo == "" || gitRef == "" { http.Error(w, "Missing 'repo' or 'gitRef' query parameter", http.StatusBadRequest) return } if gitRef == "latest" { http.Error(w, "'latest' is not a valid value for 'gitRef'. Please use an immutable image.", http.StatusBadRequest) return } cacheKey := fmt.Sprintf("%s:%s", repo, gitRef) // Check cache first if cachedResponse, ok := deps.getFromCache(cacheKey); ok { w.WriteHeader(cachedResponse.StatusCode) _, _ = w.Write(cachedResponse.Body) return } // Try handling as a release first releaseRecorder := &responseRecorder{ ResponseWriter: httptest.NewRecorder(), statusCode: 0, // Use a separate ResponseRecorder } deps.ReleasesHandler(releaseRecorder, r) // If release is found (not 404), cache and return if releaseRecorder.statusCode != http.StatusNotFound { w.WriteHeader(releaseRecorder.statusCode) if _, err := w.Write(releaseRecorder.body.Bytes()); err != nil { log.Printf("Error writing response: %v", err) } deps.storeInCache(cacheKey, releaseRecorder.statusCode, releaseRecorder.body.Bytes()) return } // Clear previous response before falling back w.Header().Del("Content-Type") // Ensure correct content type is set by CommitsHandler w.WriteHeader(http.StatusOK) // Reset status before falling back // Fall back to CommitsHandler deps.processAndCacheResponse(deps.CommitsHandler, w, r, cacheKey) } func (deps *HandlerDeps) processAndCacheResponse( handler http.HandlerFunc, w http.ResponseWriter, r *http.Request, cacheKey string, ) { rec := &responseRecorder{ResponseWriter: w, statusCode: 0} // Use 0 so that we can catch missing status updates handler(rec, r) // Store both status and response body deps.storeInCache(cacheKey, rec.statusCode, rec.body.Bytes()) } func (deps *HandlerDeps) getFromCache(key string) (CachedResponse, bool) { if value, ok := deps.cache.Get(key); ok { currentTime := time.Now() // Determine expiration time based on status code var expirationTime time.Time if value.StatusCode == http.StatusOK { expirationTime = time.Unix(value.Timestamp, 0).Add(deps.config.SuccessCacheDuration) } else { expirationTime = time.Unix(value.Timestamp, 0).Add(deps.config.ErrorCacheDuration) } // Check if the cache entry has expired if currentTime.After(expirationTime) { log.Printf("Cache expired for key: %s (Stored at: %s, Expired at: %s)", key, time.Unix(value.Timestamp, 0), expirationTime) deps.cache.Remove(key) return CachedResponse{}, false } log.Printf("Cache hit for key: %s (Stored at: %s, Expires at: %s), Status: %d", key, time.Unix(value.Timestamp, 0), expirationTime, value.StatusCode) return value, true } log.Printf("Cache miss for key: %s", key) return CachedResponse{}, false } func (deps *HandlerDeps) storeInCache(key string, statusCode int, value []byte) { timestamp := time.Now().Unix() // Store current timestamp deps.cache.Add(key, CachedResponse{ StatusCode: statusCode, Body: value, Timestamp: timestamp, }) log.Printf("Cached response for key: %s, Status: %d (Stored at %d)", key, statusCode, timestamp) }