Skip to content

Request handling ​

Learn how to interact with inbound HTTP requests inside your Go plugins. Combine this reference with the lifecycle information in Plugin Phases to pick the best hook.

HelperDescription
req.Method()HTTP method (GET, POST, …).
req.Path() / req.URL()Request path / full URL.
req.Query()Raw query string.
req.Params()Route parameters (map[string]string).
req.Header(name)Single header lookup.
req.Headers()All headers via .Get() / .GetAll().
req.RawBody()Request body (lazy read).
req.Host() / req.ClientIP()Host header and client IP.
req.Bytes() / req.Timestamp()Body size and request timestamp.

Request Object ​

Access request information through the Request object:

go
phase.RequestFilter(func(ctx *sdk.PhaseRequestFilter) {
    req := ctx.Request()
    
    // Request details
    method := req.Method()      // GET, POST, etc.
    url := req.URL()           // Full URL
    path := req.Path()         // /api/users
    query := req.Query()       // ?key=value
    host := req.Host()         // example.com
    clientIP := req.ClientIP() // 192.168.1.1
    
    ctx.Next()
})

Request Methods ​

Method() ​

Get HTTP method:

go
method := req.Method()
// "GET", "POST", "PUT", "DELETE", etc.

if method == "POST" {
    // Handle POST request
}

URL() ​

Get full URL (excluding standard ports):

go
url := req.URL()
// http://example.com/api/users?id=123
// https://example.com/api/users (no :443)

fmt.Printf("Full URL: %s\n", url)

Path() ​

Get request path:

go
path := req.Path()
// /api/users/123

if strings.HasPrefix(path, "/api/") {
    // API request
}

Query() ​

Get query string:

go
query := req.Query()
// key1=value1&key2=value2

// Parse query parameters
params, _ := url.ParseQuery(query)
id := params.Get("id")

Params() ​

Get path parameters (from route matching):

go
// Route: /users/{id}/posts/{post_id}
params := req.Params()

userID := params["id"]      // "123"
postID := params["post_id"] // "456"

Host() ​

Get hostname:

go
host := req.Host()
// example.com
// api.example.com:8080

if host == "admin.example.com" {
    // Admin request
}

ClientIP() ​

Get client IP address:

go
clientIP := req.ClientIP()
// 192.168.1.1
// 10.0.0.1

fmt.Printf("Request from: %s\n", clientIP)

Headers() ​

Get all request headers:

go
headers := req.Headers().GetAll()
// map[string]string

userAgent := headers["user-agent"]
contentType := headers["content-type"]
auth := headers["authorization"]

Get single header:

go
userAgent := req.Header("User-Agent")
auth := req.Header("Authorization")
apiKey := req.Header("X-API-Key")

if apiKey == "" {
    res := ctx.Response()
    res.RemoveHeader("Content-Length")
    res.SetHeader("Transfer-Encoding", "chunked")
    res.SetStatus(401)
    res.BodyText("Missing API key")
    ctx.End()
    return
}

Bytes() ​

Get request body size:

go
bytes := req.Bytes()
// Request content-length in bytes

fmt.Printf("Request size: %d bytes\n", bytes)

Timestamp() ​

Get request timestamp (milliseconds since epoch):

go
timestamp := req.Timestamp()
// 1704067200000

fmt.Printf("Request time: %d\n", timestamp)

Examples ​

Authentication ​

go
phase.RequestFilter(func(ctx *sdk.PhaseRequestFilter) {
    req := ctx.Request()
    
    // Check API key
    apiKey := req.Header("X-API-Key")
    if apiKey == "" {
        res := ctx.Response()
        res.RemoveHeader("Content-Length")
        res.SetHeader("Transfer-Encoding", "chunked")
        res.SetStatus(401)
        res.BodyText("Missing API key")
        ctx.End()
        return
    }
    
    if !validateAPIKey(apiKey) {
        res := ctx.Response()
        res.RemoveHeader("Content-Length")
        res.SetHeader("Transfer-Encoding", "chunked")
        res.SetStatus(401)
        res.BodyText("Invalid API key")
        ctx.End()
        return
    }
    
    ctx.Next()
})

Rate Limiting by IP ​

go
var rateLimiter = make(map[string]int)
var mu sync.Mutex

phase.RequestFilter(func(ctx *sdk.PhaseRequestFilter) {
    req := ctx.Request()
    clientIP := req.ClientIP()
    
    mu.Lock()
    count := rateLimiter[clientIP]
    count++
    rateLimiter[clientIP] = count
    mu.Unlock()
    
    if count > 100 {
        res := ctx.Response()
        res.RemoveHeader("Content-Length")
        res.SetHeader("Transfer-Encoding", "chunked")
        res.SetStatus(429)
        res.BodyText("Too many requests")
        ctx.End()
        return
    }
    
    ctx.Next()
})

Method Filtering ​

go
phase.RequestFilter(func(ctx *sdk.PhaseRequestFilter) {
    req := ctx.Request()
    
    // Only allow GET and POST
    if req.Method() != "GET" && req.Method() != "POST" {
        res := ctx.Response()
        res.RemoveHeader("Content-Length")
        res.SetHeader("Transfer-Encoding", "chunked")
        res.SetStatus(405)
        res.BodyText("Method not allowed")
        ctx.End()
        return
    }
    
    ctx.Next()
})

Path-Based Routing ​

go
phase.RequestFilter(func(ctx *sdk.PhaseRequestFilter) {
    req := ctx.Request()
    path := req.Path()
    
    if strings.HasPrefix(path, "/admin/") {
        // Check admin permission
        if !isAdmin(req.Header("Authorization")) {
            res := ctx.Response()
            res.RemoveHeader("Content-Length")
            res.SetHeader("Transfer-Encoding", "chunked")
            res.SetStatus(403)
            res.BodyText("Admin access required")
            ctx.End()
            return
        }
    }
    
    ctx.Next()
})

Request Logging ​

go
phase.RequestFilter(func(ctx *sdk.PhaseRequestFilter) {
    req := ctx.Request()
    
    log.Printf("[%s] %s %s from %s",
        req.Method(),
        req.Path(),
        req.Host(),
        req.ClientIP(),
    )
    
    // Add request ID
    requestID := uuid.New().String()
    ctx.Response().SetHeader("X-Request-ID", requestID)
    
    ctx.Next()
})

Query Parameter Validation ​

go
phase.RequestFilter(func(ctx *sdk.PhaseRequestFilter) {
    req := ctx.Request()
    
    query := req.Query()
    params, _ := url.ParseQuery(query)
    
    // Require API version
    version := params.Get("v")
    if version == "" {
        res := ctx.Response()
        res.RemoveHeader("Content-Length")
        res.SetHeader("Transfer-Encoding", "chunked")
        res.SetStatus(400)
        res.BodyText("API version required")
        ctx.End()
        return
    }
    
    if version != "1" && version != "2" {
        res := ctx.Response()
        res.RemoveHeader("Content-Length")
        res.SetHeader("Transfer-Encoding", "chunked")
        res.SetStatus(400)
        res.BodyText("Invalid API version")
        ctx.End()
        return
    }
    
    ctx.Next()
})

Host-Based Access Control ​

go
var allowedHosts = map[string]bool{
    "api.example.com": true,
    "api-staging.example.com": true,
}

phase.RequestFilter(func(ctx *sdk.PhaseRequestFilter) {
    req := ctx.Request()
    host := req.Host()
    
    // Remove port if present
    if idx := strings.Index(host, ":"); idx != -1 {
        host = host[:idx]
    }
    
    if !allowedHosts[host] {
        res := ctx.Response()
        res.RemoveHeader("Content-Length")
        res.SetHeader("Transfer-Encoding", "chunked")
        res.SetStatus(403)
        res.BodyText("Host not allowed")
        ctx.End()
        return
    }
    
    ctx.Next()
})

User Agent Blocking ​

go
var blockedAgents = []string{"bot", "crawler", "spider"}

phase.RequestFilter(func(ctx *sdk.PhaseRequestFilter) {
    req := ctx.Request()
    userAgent := strings.ToLower(req.Header("User-Agent"))
    
    for _, blocked := range blockedAgents {
        if strings.Contains(userAgent, blocked) {
            res := ctx.Response()
            res.RemoveHeader("Content-Length")
            res.SetHeader("Transfer-Encoding", "chunked")
            res.SetStatus(403)
            res.BodyText("Blocked")
            ctx.End()
            return
        }
    }
    
    ctx.Next()
})

Path Parameter Extraction ​

go
// Route: /users/{id}/posts/{post_id}

phase.RequestFilter(func(ctx *sdk.PhaseRequestFilter) {
    req := ctx.Request()
    params := req.Params()
    
    userID := params["id"]
    postID := params["post_id"]
    
    // Validate IDs
    if userID == "" || postID == "" {
        res := ctx.Response()
        res.RemoveHeader("Content-Length")
        res.SetHeader("Transfer-Encoding", "chunked")
        res.SetStatus(400)
        res.BodyText("Invalid parameters")
        ctx.End()
        return
    }
    
    // Add to headers for backend
    ctx.Response().SetHeader("X-User-ID", userID)
    ctx.Response().SetHeader("X-Post-ID", postID)
    
    ctx.Next()
})

Request Size Limit ​

go
phase.RequestFilter(func(ctx *sdk.PhaseRequestFilter) {
    req := ctx.Request()
    
    // Limit to 10MB
    maxSize := int64(10 * 1024 * 1024)
    if req.Bytes() > maxSize {
        res := ctx.Response()
        res.RemoveHeader("Content-Length")
        res.SetHeader("Transfer-Encoding", "chunked")
        res.SetStatus(413)
        res.BodyText("Request too large")
        ctx.End()
        return
    }
    
    ctx.Next()
})

Working with Payload ​

ctx.GetPayload() exposes the static payload configured for the middleware entry in YAML.
Use it to pass configuration or constants into your handler:

yaml
middleware:
  - plugin: audit-plugin
    entry: "audit"
    payload:
      log_level: "debug"
      team: "platform"
go
phase.RequestFilter(func(ctx *sdk.PhaseRequestFilter) {
    payload := ctx.GetPayload()
    logLevel, _ := payload["log_level"].(string)
    team, _ := payload["team"].(string)

    log.Printf("[audit] team=%s level=%s path=%s",
        team,
        logLevel,
        ctx.Request().Path(),
    )

    ctx.Next()
})

If you need per-request state across phases, keep it in your own map keyed by request metadata until the SDK exposes a first-class API.

Best Practices ​

1. Fail Fast ​

go
// ✅ Good
if apiKey == "" {
    res := ctx.Response()
    res.RemoveHeader("Content-Length")
    res.SetHeader("Transfer-Encoding", "chunked")
    res.SetStatus(401)
    res.BodyText("Unauthorized")
    ctx.End()
    return
}
ctx.Next()

// ❌ Bad
if apiKey != "" {
    ctx.Next()
}
// Continues even if unauthorized

2. Use Early Returns ​

go
// ✅ Good
if !authorized {
    res := ctx.Response()
    res.RemoveHeader("Content-Length")
    res.SetHeader("Transfer-Encoding", "chunked")
    res.SetStatus(403)
    res.BodyText("Forbidden")
    ctx.End()
    return
}

if !validMethod {
    res := ctx.Response()
    res.RemoveHeader("Content-Length")
    res.SetHeader("Transfer-Encoding", "chunked")
    res.SetStatus(405)
    res.BodyText("Method not allowed")
    ctx.End()
    return
}

ctx.Next()

3. Log Important Events ​

go
req := ctx.Request()
log.Printf("[%s] %s %s from %s", 
    req.Method(), req.Path(), req.Host(), req.ClientIP())

4. Validate Input ​

go
// Always validate before use
params := req.Params()
id := params["id"]

if id == "" {
    res := ctx.Response()
    res.RemoveHeader("Content-Length")
    res.SetHeader("Transfer-Encoding", "chunked")
    res.SetStatus(400)
    res.BodyText("Missing ID")
    ctx.End()
    return
}

5. Set Response Headers Early ​

go
// Set before calling Next()
ctx.Response().SetHeader("X-Request-ID", requestID)
ctx.Next()

See Also ​

Released under the MIT License.