forked from aegis/pyserveX
feat(caching): Implement cache hit detection and response header management
- Added functionality to mark responses as cache hits to prevent incorrect X-Cache headers. - Introduced setCacheHitFlag function to traverse response writer wrappers and set cache hit flag. - Updated cachingResponseWriter to manage cache hit state and adjust X-Cache header accordingly. - Enhanced ProcessRequest and ProcessResponse methods to utilize new caching logic. feat(extension): Introduce ResponseWriterWrapper and ResponseFinalizer interfaces - Added ResponseWriterWrapper interface for extensions to wrap response writers. - Introduced ResponseFinalizer interface for finalizing responses after processing. refactor(manager): Improve response writer wrapping and finalization - Updated Manager.Handler to wrap response writers through all enabled extensions. - Implemented finalization of response writers after processing requests. test(caching): Add comprehensive integration tests for caching behavior - Created caching_test.go with tests for cache hit/miss, TTL expiration, pattern-based caching, and more. - Ensured that caching logic works correctly for various scenarios including query strings and error responses. test(routing): Add integration tests for routing behavior - Created routing_test.go with tests for route priority, case sensitivity, default routes, and return directives. - Verified that routing behaves as expected with multiple regex routes and named groups.
This commit is contained in:
@@ -137,6 +137,10 @@ func (e *CachingExtension) ProcessRequest(ctx context.Context, w http.ResponseWr
|
||||
e.hits++
|
||||
e.mu.Unlock()
|
||||
|
||||
// Mark as cache hit to prevent setting X-Cache: MISS
|
||||
// Try to find cachingResponseWriter in the wrapper chain
|
||||
setCacheHitFlag(w)
|
||||
|
||||
for k, values := range entry.headers {
|
||||
for _, v := range values {
|
||||
w.Header().Add(k, v)
|
||||
@@ -158,11 +162,36 @@ func (e *CachingExtension) ProcessRequest(ctx context.Context, w http.ResponseWr
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// setCacheHitFlag tries to find cachingResponseWriter and set cache hit flag
|
||||
func setCacheHitFlag(w http.ResponseWriter) {
|
||||
// Direct match
|
||||
if cw, ok := w.(*cachingResponseWriter); ok {
|
||||
cw.SetCacheHit()
|
||||
return
|
||||
}
|
||||
|
||||
// Try unwrapping
|
||||
type unwrapper interface {
|
||||
Unwrap() http.ResponseWriter
|
||||
}
|
||||
|
||||
for {
|
||||
if u, ok := w.(unwrapper); ok {
|
||||
w = u.Unwrap()
|
||||
if cw, ok := w.(*cachingResponseWriter); ok {
|
||||
cw.SetCacheHit()
|
||||
return
|
||||
}
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ProcessResponse caches the response if applicable
|
||||
func (e *CachingExtension) ProcessResponse(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
||||
// Response caching is handled by the CachingResponseWriter
|
||||
// This is called after the response is written
|
||||
w.Header().Set("X-Cache", "MISS")
|
||||
// X-Cache header is set in the cachingResponseWriter.WriteHeader
|
||||
}
|
||||
|
||||
// WrapResponseWriter wraps the response writer to capture the response for caching
|
||||
@@ -186,16 +215,26 @@ type cachingResponseWriter struct {
|
||||
buffer *bytes.Buffer
|
||||
statusCode int
|
||||
wroteHeader bool
|
||||
cacheHit bool // Flag to indicate if this was a cache hit
|
||||
}
|
||||
|
||||
func (cw *cachingResponseWriter) WriteHeader(code int) {
|
||||
if !cw.wroteHeader {
|
||||
cw.statusCode = code
|
||||
cw.wroteHeader = true
|
||||
// Set X-Cache: MISS header before writing headers (only if not a cache hit)
|
||||
if !cw.cacheHit {
|
||||
cw.ResponseWriter.Header().Set("X-Cache", "MISS")
|
||||
}
|
||||
cw.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
}
|
||||
|
||||
// SetCacheHit marks this response as a cache hit (to avoid setting X-Cache: MISS)
|
||||
func (cw *cachingResponseWriter) SetCacheHit() {
|
||||
cw.cacheHit = true
|
||||
}
|
||||
|
||||
func (cw *cachingResponseWriter) Write(b []byte) (int, error) {
|
||||
if !cw.wroteHeader {
|
||||
cw.WriteHeader(http.StatusOK)
|
||||
|
||||
@@ -109,3 +109,15 @@ type ExtensionConfig struct {
|
||||
|
||||
// ExtensionFactory is a function that creates an extension from config
|
||||
type ExtensionFactory func(config map[string]interface{}, logger *logging.Logger) (Extension, error)
|
||||
|
||||
// ResponseWriterWrapper is an optional interface that extensions can implement
|
||||
// to wrap the response writer for capturing/modifying responses
|
||||
type ResponseWriterWrapper interface {
|
||||
WrapResponseWriter(w http.ResponseWriter, r *http.Request) http.ResponseWriter
|
||||
}
|
||||
|
||||
// ResponseFinalizer is an optional interface for response writers that need
|
||||
// to perform finalization after the response is written (e.g., caching)
|
||||
type ResponseFinalizer interface {
|
||||
Finalize()
|
||||
}
|
||||
|
||||
@@ -177,26 +177,58 @@ func (m *Manager) Handler(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// Create response wrapper to capture response for ProcessResponse
|
||||
wrapper := newResponseWrapper(w)
|
||||
// Wrap response writer through all extensions that support it
|
||||
// Process in reverse priority order so highest priority wrapper is outermost
|
||||
wrappedWriter := w
|
||||
var finalizers []ResponseFinalizer
|
||||
|
||||
m.mu.RLock()
|
||||
extensions := m.extensions
|
||||
m.mu.RUnlock()
|
||||
|
||||
// Wrap response writer (lowest priority first, so they wrap in correct order)
|
||||
for _, ext := range extensions {
|
||||
if !ext.Enabled() {
|
||||
continue
|
||||
}
|
||||
if wrapper, ok := ext.(ResponseWriterWrapper); ok {
|
||||
wrappedWriter = wrapper.WrapResponseWriter(wrappedWriter, r)
|
||||
// Check if the wrapped writer implements Finalizer
|
||||
if finalizer, ok := wrappedWriter.(ResponseFinalizer); ok {
|
||||
finalizers = append(finalizers, finalizer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create response wrapper to capture status code
|
||||
responseWrapper := newResponseWrapper(wrappedWriter)
|
||||
|
||||
// Process request through extensions
|
||||
handled, err := m.ProcessRequest(ctx, wrapper, r)
|
||||
handled, err := m.ProcessRequest(ctx, responseWrapper, r)
|
||||
if err != nil {
|
||||
m.logger.Error("Error processing request", "error", err)
|
||||
}
|
||||
|
||||
if handled {
|
||||
// Extension handled the request, process response
|
||||
m.ProcessResponse(ctx, wrapper, r)
|
||||
m.ProcessResponse(ctx, responseWrapper, r)
|
||||
// Finalize all response writers
|
||||
for i := len(finalizers) - 1; i >= 0; i-- {
|
||||
finalizers[i].Finalize()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// No extension handled, pass to next handler
|
||||
next.ServeHTTP(wrapper, r)
|
||||
next.ServeHTTP(responseWrapper, r)
|
||||
|
||||
// Process response
|
||||
m.ProcessResponse(ctx, wrapper, r)
|
||||
m.ProcessResponse(ctx, responseWrapper, r)
|
||||
|
||||
// Finalize all response writers
|
||||
for i := len(finalizers) - 1; i >= 0; i-- {
|
||||
finalizers[i].Finalize()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -232,3 +264,8 @@ func (rw *responseWrapper) Write(b []byte) (int, error) {
|
||||
func (rw *responseWrapper) StatusCode() int {
|
||||
return rw.statusCode
|
||||
}
|
||||
|
||||
// Unwrap returns the underlying ResponseWriter (for type assertions)
|
||||
func (rw *responseWrapper) Unwrap() http.ResponseWriter {
|
||||
return rw.ResponseWriter
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user