Custom Middleware

Compiled Go services support custom middleware for advanced use cases like authentication, logging, request modification, and stateful behavior.

Overview

Custom middleware is prepended before the built-in middleware chain:

Request → Resource Resolver → Config Override → [Custom Middleware] → Latency/Error → Replay Read/Write → Cache Read → Upstream → Cache Write → Handler → Response

Adding Custom Middleware

Edit middleware.go in your service directory:

package petstore

import (
    "net/http"
    "github.com/mockzilla/mockzilla/v2/pkg/middleware"
)

func getMiddleware() []func(*middleware.Params) func(http.Handler) http.Handler {
    return []func(*middleware.Params) func(http.Handler) http.Handler{
        createAuthMiddleware,
        createLoggingMiddleware,
    }
}

func createAuthMiddleware(params *middleware.Params) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            token := r.Header.Get("Authorization")
            if token == "" {
                http.Error(w, "Unauthorized", http.StatusUnauthorized)
                return
            }
            next.ServeHTTP(w, r)
        })
    }
}

Middleware Params

The middleware.Params struct provides access to service configuration and request history:

type Params struct {
    ServiceConfig *config.ServiceConfig  // Service configuration
    History       *history.CurrentRequestStorage  // Request/response history
}

ServiceConfig

Access service configuration values:

func createConfigAwareMiddleware(params *middleware.Params) func(http.Handler) http.Handler {
    serviceName := params.ServiceConfig.Name
    
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            log.Printf("Service: %s, Path: %s", serviceName, r.URL.Path)
            next.ServeHTTP(w, r)
        })
    }
}

Request History

Access the current request and previous requests/responses:

func createHistoryMiddleware(params *middleware.Params) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            // Get current request record
            record, exists := params.History.Get(r)
            if exists {
                log.Printf("Request body: %s", string(record.Body))
                if record.Response != nil {
                    log.Printf("Previous response: %d", record.Response.StatusCode)
                }
            }
            
            next.ServeHTTP(w, r)
        })
    }
}

RequestedResource

The history record contains:

type RequestedResource struct {
    Resource       string           // OpenAPI path: /pets/{id}
    Body           []byte           // Request body
    Response       *HistoryResponse // Previous response (if any)
    Request        *http.Request    // Current HTTP request
    ServiceStorage Storage          // Per-service key-value storage
}

Service Storage

Each service has a thread-safe key-value storage for maintaining state across requests:

func createStatefulMiddleware(params *middleware.Params) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            record, _ := params.History.Get(r)
            storage := record.ServiceStorage
            
            // Increment request counter
            count, _ := storage.Get("request_count")
            if count == nil {
                count = 0
            }
            storage.Set("request_count", count.(int) + 1)
            
            // Store user session
            userID := r.Header.Get("X-User-ID")
            if userID != "" {
                storage.Set("last_user", userID)
            }
            
            next.ServeHTTP(w, r)
        })
    }
}

Storage Interface

type Storage interface {
    Get(key string) (any, bool)
    Set(key string, value any)
    Data() map[string]any
}

Note: history.duration sets how long entries are retained (default 60m, configurable per service, 0 = no expiry). Enforcement depends on the storage driver: the in-memory driver filters out entries older than the duration on read and never actively sweeps them (memory is reclaimed only on clear or restart); the Redis driver applies the duration as the key TTL and lets Redis expire them. Mockzilla runs no background cleanup loop of its own.

History Transform

Register a callback to modify history entries before they are saved. This runs before the mask-headers config is applied, so you can implement custom redaction logic.

params.SetHistoryTransform(func(req *db.HistoryRequest, resp *db.HistoryResponse) {
    // Redact request body
    if len(req.Body) > 0 {
        req.Body = []byte("[redacted]")
    }

    // Remove specific headers entirely
    filtered := req.Headers[:0]
    for _, h := range req.Headers {
        if !strings.HasPrefix(h, "X-Internal-") {
            filtered = append(filtered, h)
        }
    }
    req.Headers = filtered
})

The callback receives pointers to the request and response structs and can modify them in place. After the callback returns, any mask-headers patterns from the service config are applied on top.

Common Patterns

Request Logging

func createLoggingMiddleware(params *middleware.Params) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            start := time.Now()
            log.Printf("→ %s %s", r.Method, r.URL.Path)
            
            next.ServeHTTP(w, r)
            
            log.Printf("← %s %s (%v)", r.Method, r.URL.Path, time.Since(start))
        })
    }
}

Request Modification

func createHeaderMiddleware(params *middleware.Params) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            // Add headers to response
            w.Header().Set("X-Service", params.ServiceConfig.Name)
            w.Header().Set("X-Request-ID", uuid.New().String())
            
            next.ServeHTTP(w, r)
        })
    }
}

Conditional Logic

func createConditionalMiddleware(params *middleware.Params) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            // Only apply to specific paths
            if strings.HasPrefix(r.URL.Path, "/admin") {
                if r.Header.Get("X-Admin-Token") != "secret" {
                    http.Error(w, "Forbidden", http.StatusForbidden)
                    return
                }
            }
            
            next.ServeHTTP(w, r)
        })
    }
}