Skip to main content

Overview

The mirror middleware duplicates requests to one or more target servers for traffic shadowing, testing, and analysis. Mirrored requests run asynchronously and don’t affect the original response.

Installation

import "github.com/go-mizu/mizu/middlewares/mirror"

Quick Start

app := mizu.New()

// Mirror all traffic to staging server
app.Use(mirror.New("https://staging.example.com"))

Configuration

OptionTypeDefaultDescription
Targets[]Target-Mirror target URLs
Timeouttime.Duration5sTimeout for mirrored requests
AsyncbooltrueRun mirror requests asynchronously
CopyBodybooltrueCopy request body
OnErrorfunc(string, error)-Error callback
OnSuccessfunc(string, *http.Response)-Success callback

Examples

Single Target

app.Use(mirror.New("https://staging.example.com"))

// All requests are mirrored to staging
app.Get("/api/users", usersHandler)

Multiple Targets

app.Use(mirror.New(
    "https://staging.example.com",
    "https://analytics.example.com",
))

Percentage-Based Mirroring

app.Use(mirror.WithOptions(mirror.Options{
    Targets: []mirror.Target{
        mirror.Percentage("https://staging.example.com", 50), // 50% of traffic
        mirror.Percentage("https://canary.example.com", 10),  // 10% of traffic
    },
}))

Custom Timeout

app.Use(mirror.WithOptions(mirror.Options{
    Targets: []mirror.Target{
        {URL: "https://staging.example.com", Percentage: 100},
    },
    Timeout: 10 * time.Second,
}))

Synchronous Mirroring

// Wait for mirror requests to complete
app.Use(mirror.WithOptions(mirror.Options{
    Targets: []mirror.Target{
        {URL: "https://backup.example.com", Percentage: 100},
    },
    Async: false,
}))

Error Handling

app.Use(mirror.WithOptions(mirror.Options{
    Targets: []mirror.Target{
        {URL: "https://staging.example.com", Percentage: 100},
    },
    OnError: func(target string, err error) {
        log.Printf("Mirror to %s failed: %v", target, err)
        metrics.IncrementCounter("mirror_errors", target)
    },
}))

Success Callback

app.Use(mirror.WithOptions(mirror.Options{
    Targets: []mirror.Target{
        {URL: "https://staging.example.com", Percentage: 100},
    },
    OnSuccess: func(target string, resp *http.Response) {
        log.Printf("Mirror to %s: status %d", target, resp.StatusCode)

        // Compare responses
        if resp.StatusCode >= 500 {
            alertOps("Staging returned error")
        }
    },
}))

Response Comparison

var mu sync.Mutex
var prodResponses = make(map[string]int)
var stagingResponses = make(map[string]int)

app.Use(mirror.WithOptions(mirror.Options{
    Targets: []mirror.Target{
        {URL: "https://staging.example.com", Percentage: 100},
    },
    OnSuccess: func(target string, resp *http.Response) {
        mu.Lock()
        defer mu.Unlock()
        stagingResponses[resp.Request.URL.Path] = resp.StatusCode
    },
}))

// Middleware to track production responses
app.Use(func(next mizu.Handler) mizu.Handler {
    return func(c *mizu.Ctx) error {
        err := next(c)
        // Track and compare...
        return err
    }
})

Route-Specific Mirroring

// Only mirror specific routes
api := app.Group("/api")
api.Use(mirror.New("https://staging.example.com"))

// Other routes are not mirrored
app.Get("/health", healthHandler)

Canary Testing

// Mirror small percentage to canary for testing
app.Use(mirror.WithOptions(mirror.Options{
    Targets: []mirror.Target{
        mirror.Percentage("https://canary.example.com", 5),
    },
    OnSuccess: func(target string, resp *http.Response) {
        if resp.StatusCode >= 500 {
            // Alert on canary errors
            alertCanaryError(target, resp.StatusCode)
        }
    },
    OnError: func(target string, err error) {
        alertCanaryError(target, -1)
    },
}))

A/B Testing Analysis

app.Use(mirror.WithOptions(mirror.Options{
    Targets: []mirror.Target{
        {URL: "https://experiment-a.example.com", Percentage: 50},
        {URL: "https://experiment-b.example.com", Percentage: 50},
    },
    OnSuccess: func(target string, resp *http.Response) {
        // Log response times for analysis
        latency := resp.Header.Get("X-Response-Time")
        analytics.TrackLatency(target, latency)
    },
}))

Skip Body for GET Requests

app.Use(mirror.WithOptions(mirror.Options{
    Targets: []mirror.Target{
        {URL: "https://staging.example.com", Percentage: 100},
    },
    CopyBody: false, // Don't copy body (faster for GET)
}))

Target Structure

type Target struct {
    URL        string // Target base URL
    Percentage int    // Percentage of requests to mirror (0-100)
}

Helper Functions

// Create target with percentage
mirror.Percentage("https://example.com", 50) // 50% of traffic

Mirrored Request Headers

Each mirrored request includes:
  • All original headers
  • X-Mirrored-From: <original-host>

API Reference

func New(targets ...string) mizu.Middleware
func WithOptions(opts Options) mizu.Middleware
func Percentage(url string, pct int) Target

Use Cases

  1. Shadow Testing: Test new versions with production traffic
  2. Load Testing: Send copies to test infrastructure
  3. Analytics: Duplicate to analytics services
  4. Backup: Sync to backup systems
  5. Canary Deployment: Route small percentage to canary

Technical Details

Implementation Architecture

The mirror middleware is built with the following core components:
  1. Request Cloning: The middleware reads the original request body (if CopyBody is enabled) and stores it in memory to allow multiple sends without consuming the original request stream.
  2. HTTP Client: A dedicated http.Client is created with the configured timeout and redirect policy (http.ErrUseLastResponse to prevent following redirects).
  3. Percentage-Based Sampling: Uses a simple counter-based modulo operation to determine whether to mirror a request based on the configured percentage (e.g., 50% mirrors every other request).
  4. Asynchronous Execution: When Async is true (default), mirrored requests are executed in separate goroutines, ensuring zero impact on the main request latency.

Request Flow

  1. Original request arrives at the middleware
  2. If CopyBody is enabled and body exists, read body into memory and restore it to the original request
  3. For each target:
    • Check percentage threshold using counter-based sampling
    • If threshold met, execute mirror request (async or sync based on configuration)
  4. Continue to next middleware/handler with original request unchanged

Mirror Request Details

Each mirrored request includes:
  • All original HTTP headers copied verbatim
  • X-Mirrored-From header set to original request host
  • Same HTTP method as original request
  • Same request URI (path + query string) appended to target base URL
  • Request body (if CopyBody is enabled)

Performance Characteristics

  • Async mode: Zero latency impact on original request (default)
  • Sync mode: Adds mirror request latency to original request processing
  • Memory overhead: Proportional to request body size when CopyBody is enabled
  • Goroutine cost: One goroutine per target per mirrored request in async mode

Error Handling

  • Mirror failures never affect the original request/response
  • Errors are silently ignored unless OnError callback is provided
  • HTTP client respects the configured timeout to prevent hanging requests
  • Response bodies are properly closed to prevent resource leaks

Best Practices

  • Use async mode (default) to avoid latency
  • Set appropriate timeouts
  • Monitor error rates on mirror targets
  • Use percentage-based mirroring for gradual rollouts
  • Log and compare responses for validation
  • Don’t mirror to targets with side effects (unless intended)

Testing

The mirror middleware includes comprehensive test coverage for all features:
Test CaseDescriptionExpected Behavior
TestNewBasic mirror functionality with single targetRequest is successfully mirrored to target server
TestWithOptions_AsyncAsynchronous mirroring behaviorOriginal response returns immediately without waiting for mirror completion
TestWithOptions_PercentagePercentage-based traffic samplingApproximately 50% of requests are mirrored when percentage is set to 50
TestWithOptions_MultipleTargetsMirroring to multiple targets simultaneouslyAll configured targets receive mirrored requests
TestWithOptions_OnErrorError callback invocationOnError callback is called when mirror request fails
TestWithOptions_OnSuccessSuccess callback invocationOnSuccess callback is called with response when mirror succeeds
TestWithOptions_MirroredHeaderX-Mirrored-From header injectionMirror request includes X-Mirrored-From header with original host
TestPercentagePercentage helper functionReturns Target with correct URL and percentage values
TestWithOptions_CopyBodyRequest body copying enabledMirror request receives the same body as original request
TestWithOptions_NoCopyBodyRequest body copying disabledMirror request is sent without body content
TestNew_MultipleTargetsMultiple targets via New() helperAll targets receive mirrored requests with async execution