Skip to content

Custom Middleware

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

Overview

Custom middleware is applied after the standard middleware chain:

Request → [Standard Middleware] → [Custom Middleware] → Handler → Response

Standard: Config Override → Latency/Error → Replay Read/Write → Cache Read → Upstream → Cache Write

Standard middleware handles: latency, errors, replay, caching, upstream proxy.

Adding Custom Middleware

Edit middleware.go in your service directory:

package petstore

import (
    "net/http"
    "github.com/cubahno/connexions/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: Storage is cleared periodically based on historyDuration setting (default: 5 minutes).

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)
        })
    }
}