Skip to main content
The proxy.go convention allows you to intercept and modify requests before they reach your route handlers. Inspired by Next.js middleware, Fuego’s proxy runs at the edge of your request flow.

Overview

Proxy runs before route matching, giving you the power to:
  • Rewrite URLs - Change the internal path without changing the browser URL (A/B testing, feature flags)
  • Redirect - Send users to different URLs (301/302/307/308 redirects)
  • Respond Early - Return responses without hitting your handlers (auth checks, rate limiting)
  • Modify Requests - Add headers, set cookies, or transform requests

Quick Start

Create app/proxy.go:
package app

import "github.com/abdul-hamid-achik/fuego/pkg/fuego"

func Proxy(c *fuego.Context) (*fuego.ProxyResult, error) {
    // Redirect old URLs
    if c.Path() == "/old-page" {
        return fuego.Redirect("/new-page", 301), nil
    }
    
    // Continue to normal routing
    return fuego.Continue(), nil
}

Request Flow

Request → Proxy → Global Middleware → Route Middleware → Handler
The proxy runs first, before any middleware or route handlers.

ProxyResult Helpers

Continue

Proceed with normal routing:
return fuego.Continue(), nil

Redirect

Send an HTTP redirect:
// Permanent redirect (301)
return fuego.Redirect("/new-page", 301), nil

// Temporary redirect (302)
return fuego.Redirect("/temp-page", 302), nil

// Preserve method (307/308)
return fuego.Redirect("/api/v2/users", 308), nil

Rewrite

Change the internal path (URL bar stays the same):
// User sees /products but server handles /catalog/products
return fuego.Rewrite("/catalog" + c.Path()), nil

Response

Return a response directly, bypassing routing:
// JSON response
return fuego.ResponseJSON(403, `{"error":"forbidden"}`), nil

// HTML response
return fuego.ResponseHTML(503, "<h1>Maintenance</h1>"), nil

// Custom response
return fuego.Response(429, []byte("Rate limited"), "text/plain"), nil

Adding Headers

Add headers to redirects or responses:
return fuego.Redirect("/login", 302).WithHeader("X-Reason", "session-expired"), nil

return fuego.ResponseJSON(200, `{}`).WithHeaders(map[string]string{
    "X-Custom-1": "value1",
    "X-Custom-2": "value2",
}), nil

ProxyConfig (Optional)

Limit which paths run through the proxy:
package app

import "github.com/abdul-hamid-achik/fuego/pkg/fuego"

// ProxyConfig is optional - if not defined, proxy runs on all paths
var ProxyConfig = &fuego.ProxyConfig{
    Matcher: []string{
        "/api/:path*",     // All API routes
        "/admin/*",        // Admin routes
    },
}

func Proxy(c *fuego.Context) (*fuego.ProxyResult, error) {
    // This only runs for paths matching the patterns above
    return fuego.Continue(), nil
}

Matcher Patterns

PatternDescription
/api/usersExact match
/api/:paramSingle dynamic segment
/api/:path*Zero or more segments (wildcard)
/api/:path+One or more segments
/api/:param?Optional segment
/(api|admin)Regex group

Use Cases

Authentication

func Proxy(c *fuego.Context) (*fuego.ProxyResult, error) {
    // Skip auth for public paths
    if c.Path() == "/" || c.Path() == "/login" {
        return fuego.Continue(), nil
    }
    
    token := c.Header("Authorization")
    if token == "" {
        return fuego.Redirect("/login", 302), nil
    }
    
    if !isValidToken(token) {
        return fuego.ResponseJSON(401, `{"error":"invalid token"}`), nil
    }
    
    return fuego.Continue(), nil
}

A/B Testing

func Proxy(c *fuego.Context) (*fuego.ProxyResult, error) {
    // Check experiment cookie
    variant := c.Cookie("experiment")
    
    if variant == "B" && c.Path() == "/pricing" {
        return fuego.Rewrite("/pricing-variant-b"), nil
    }
    
    return fuego.Continue(), nil
}

URL Migration

func Proxy(c *fuego.Context) (*fuego.ProxyResult, error) {
    path := c.Path()
    
    // Redirect old API version
    if strings.HasPrefix(path, "/api/v1/") {
        newPath := strings.Replace(path, "/api/v1/", "/api/v2/", 1)
        return fuego.Redirect(newPath, 308), nil
    }
    
    return fuego.Continue(), nil
}

Rate Limiting

var limiter = NewRateLimiter(100, time.Minute)

func Proxy(c *fuego.Context) (*fuego.ProxyResult, error) {
    ip := c.ClientIP()
    
    if !limiter.Allow(ip) {
        return fuego.ResponseJSON(429, `{"error":"too many requests"}`).
            WithHeader("Retry-After", "60"), nil
    }
    
    return fuego.Continue(), nil
}

Maintenance Mode

var maintenanceMode = false

func Proxy(c *fuego.Context) (*fuego.ProxyResult, error) {
    if maintenanceMode && !strings.HasPrefix(c.Path(), "/health") {
        return fuego.ResponseHTML(503, `
            <!DOCTYPE html>
            <html>
                <body>
                    <h1>We'll be back soon!</h1>
                </body>
            </html>
        `), nil
    }
    
    return fuego.Continue(), nil
}

Geolocation Routing

func Proxy(c *fuego.Context) (*fuego.ProxyResult, error) {
    country := c.Header("CF-IPCountry") // Cloudflare header
    
    if country == "DE" && c.Path() == "/" {
        return fuego.Rewrite("/de/home"), nil
    }
    
    return fuego.Continue(), nil
}

Complete Example

Here’s a full proxy implementation:
// app/proxy.go
package app

import (
    "strings"
    "github.com/abdul-hamid-achik/fuego/pkg/fuego"
)

func Proxy(c *fuego.Context) (*fuego.ProxyResult, error) {
    path := c.Path()

    // 1. Legacy URL Rewriting: /v1/* -> /api/*
    if strings.HasPrefix(path, "/v1/") {
        newPath := strings.Replace(path, "/v1/", "/api/", 1)
        return fuego.Rewrite(newPath), nil
    }

    // 2. Access Control: Block /api/admin without auth
    if strings.HasPrefix(path, "/api/admin") {
        authHeader := c.Header("Authorization")
        if authHeader == "" {
            return fuego.ResponseJSON(401, `{"error":"unauthorized"}`), nil
        }
        if authHeader != "Bearer admin-token" {
            return fuego.ResponseJSON(403, `{"error":"forbidden"}`), nil
        }
    }

    // 3. Add tracking headers for all requests
    return fuego.Continue().
        WithHeader("X-Proxy-Version", "1.0").
        WithHeader("X-Request-Path", path), nil
}

Proxy vs Middleware

FeatureProxyMiddleware
RunsBefore routingAfter routing
Can rewrite URLsYesNo
Can redirectYesYes
Access to route paramsNoYes
Per-route configurationVia matchersPer-route
File locationapp/proxy.goapp/**/middleware.go
Use Proxy when you need to:
  • Modify the URL before routing
  • Decide routing based on request properties
  • Return early responses for all paths
Use Middleware when you need to:
  • Access route parameters
  • Wrap specific route handlers
  • Apply logic after routing is determined

Proxy Logging

The app-level logger automatically captures proxy actions with helpful tags:
[12:34:56] GET /old-page 301 in 2ms [redirect → /new-page]
[12:34:57] GET /v1/users → /api/users 200 in 45ms [rewrite]
[12:34:58] GET /api/admin 403 in 1ms [proxy]

Action Tags

TagDescription
[proxy]Request handled entirely by proxy (early response)
[rewrite]URL was rewritten internally (shows original → new path)
[redirect → URL]Request was redirected to another URL

Error Handling

If your proxy function returns an error, Fuego returns a 500 Internal Server Error:
func Proxy(c *fuego.Context) (*fuego.ProxyResult, error) {
    user, err := validateSession(c)
    if err != nil {
        // Better: handle errors explicitly
        return fuego.ResponseJSON(500, `{"error":"internal error"}`), nil
    }
    
    return fuego.Continue(), nil
}
Always handle errors explicitly in your proxy. Returning an error will result in a generic 500 response.

CLI Support

View proxy configuration with the routes command:
fuego routes
Output:
  Fuego Routes

  PROXY   Proxy enabled
          Matchers: [/api/:path* /admin/*]
          File: app/proxy.go

  GET     /api/users                    app/api/users/route.go
  POST    /api/users                    app/api/users/route.go
  ...

Best Practices

Proxy runs on every request, so avoid slow operations like database queries or external API calls.
Return explicit error responses rather than returning errors. This gives you control over the response format.
Only run proxy on paths that need it. This improves performance for routes that don’t need proxy processing.
Proxy bugs affect all matching requests. Test edge cases carefully.

Next Steps