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.
| Helper | Description |
|---|---|
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:
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:
method := req.Method()
// "GET", "POST", "PUT", "DELETE", etc.
if method == "POST" {
// Handle POST request
}URL()
Get full URL (excluding standard ports):
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:
path := req.Path()
// /api/users/123
if strings.HasPrefix(path, "/api/") {
// API request
}Query()
Get query string:
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):
// Route: /users/{id}/posts/{post_id}
params := req.Params()
userID := params["id"] // "123"
postID := params["post_id"] // "456"Host()
Get hostname:
host := req.Host()
// example.com
// api.example.com:8080
if host == "admin.example.com" {
// Admin request
}ClientIP()
Get client IP address:
clientIP := req.ClientIP()
// 192.168.1.1
// 10.0.0.1
fmt.Printf("Request from: %s\n", clientIP)Headers()
Get all request headers:
headers := req.Headers().GetAll()
// map[string]string
userAgent := headers["user-agent"]
contentType := headers["content-type"]
auth := headers["authorization"]Header()
Get single header:
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:
bytes := req.Bytes()
// Request content-length in bytes
fmt.Printf("Request size: %d bytes\n", bytes)Timestamp()
Get request timestamp (milliseconds since epoch):
timestamp := req.Timestamp()
// 1704067200000
fmt.Printf("Request time: %d\n", timestamp)Examples
Authentication
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
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
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
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
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
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
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
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
// 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
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:
middleware:
- plugin: audit-plugin
entry: "audit"
payload:
log_level: "debug"
team: "platform"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
// ✅ 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 unauthorized2. Use Early Returns
// ✅ 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
req := ctx.Request()
log.Printf("[%s] %s %s from %s",
req.Method(), req.Path(), req.Host(), req.ClientIP())4. Validate Input
// 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
// Set before calling Next()
ctx.Response().SetHeader("X-Request-ID", requestID)
ctx.Next()See Also
- Response Handling - Handle responses
- Plugin Phases - Understanding phases
- Go SDK - Complete SDK reference