httputil
A comprehensive HTTP utilities library for Go that provides common HTTP functionality including JWT handling, middleware, proxying, file serving, and more.
Features
- 🍪 Cookie Management - Easy cookie setting and retrieval with security options
- 🔐 JWT Utilities - JWT encoding/decoding with custom claims support
- 🔄 HTTP Retry Client - Configurable retry logic with exponential backoff
- 🛠️ Middleware - Logging, session context, and middleware chaining
- 🔄 Reverse Proxy - Simple reverse proxy with development mode support
- 📁 File Serving - Static file serving with SPA fallback support
- 📏 Request Limiting - Request body size limiting for security
- ⚡ Zero Dependencies - Minimal external dependencies (only JWT library)
Installation
go get ella.to/httputil
Quick Start
package main
import (
"net/http"
"time"
"ella.to/httputil"
)
func main() {
// Create a retry client
client, _ := httputil.NewRetryClient(
httputil.WithMaxRetries(3),
httputil.WithHeaders(map[string]string{
"User-Agent": "my-app/1.0",
}),
)
// Set up middleware
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
httputil.SetCookie(w, "session", "abc123", 24*time.Hour, true)
w.Write([]byte("Hello World"))
})
// Chain middleware
handler := httputil.Chain(mux, httputil.WithLogging)
http.ListenAndServe(":8080", handler)
}
API Reference
Cookie Management
SetCookie
Sets an HTTP cookie with security options.
func SetCookie(w http.ResponseWriter, key CookieKey, value string, maxAge time.Duration, secure bool)
Parameters:
w - HTTP response writer
key - Cookie name (typed as CookieKey for type safety)
value - Cookie value
maxAge - Cookie expiration duration (0 deletes the cookie)
secure - Whether cookie should only be sent over HTTPS
Example:
// Set a secure session cookie for 24 hours
httputil.SetCookie(w, httputil.CookieKey("session"), "user123", 24*time.Hour, true)
// Delete a cookie
httputil.SetCookie(w, httputil.CookieKey("old_session"), "", 0, false)
GetCookie
Retrieves a cookie value from the request.
func GetCookie(key CookieKey, r *http.Request) (string, error)
Returns: Cookie value and error (wrapped with context if cookie not found)
Example:
sessionID, err := httputil.GetCookie(httputil.CookieKey("session"), r)
if err != nil {
// Handle missing or invalid cookie
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// Use sessionID...
JWT Utilities
Creating a JWT Handler
func New(secretKey string) *Jwt
Example:
jwtHandler := httputil.New("your-secret-key")
Encoding JWT
func (j *Jwt) Encode(claims JwtClaims) (string, error)
Example:
type UserClaims struct {
*httputil.JwtRegisteredClaims
UserID string `json:"user_id"`
Role string `json:"role"`
}
func (c *UserClaims) ParseToken(token *httputil.JwtToken) error {
if !token.Valid {
return errors.New("invalid token")
}
return nil
}
claims := &UserClaims{
JwtRegisteredClaims: &httputil.JwtRegisteredClaims{
ExpiresAt: httputil.JwtNewNumericDate(time.Now().Add(24 * time.Hour)),
Subject: "user123",
},
UserID: "12345",
Role: "admin",
}
token, err := jwtHandler.Encode(claims)
Decoding JWT
func (j *Jwt) Decode(jwt string, claims JwtClaims) error
Note: Claims must implement the TokenParser interface:
type TokenParser interface {
ParseToken(token *JwtToken) error
}
Example:
decodedClaims := &UserClaims{JwtRegisteredClaims: &httputil.JwtRegisteredClaims{}}
err := jwtHandler.Decode(token, decodedClaims)
if err != nil {
// Handle invalid token
}
// Use decodedClaims.UserID, decodedClaims.Role, etc.
HTTP Retry Client
Creating a Retry Client
func NewRetryClient(opts ...retryTransportOpt) (*http.Client, error)
Available Options:
WithMaxRetries(int) - Maximum number of retries (default: 3)
WithInitialDelay(time.Duration) - Initial delay between retries (default: 1s)
WithMaxDelay(time.Duration) - Maximum delay cap (default: 30s)
WithHeaders(map[string]string) - Headers to inject into every request
Example:
client, err := httputil.NewRetryClient(
httputil.WithMaxRetries(5),
httputil.WithInitialDelay(500*time.Millisecond),
httputil.WithMaxDelay(10*time.Second),
httputil.WithHeaders(map[string]string{
"User-Agent": "my-service/1.0",
"Accept": "application/json",
}),
)
// Use like any http.Client
resp, err := client.Get("https://api.example.com/data")
Retry Behavior:
- Retries on 5xx server errors and 429 (Too Many Requests)
- Uses exponential backoff with jitter
- Preserves request body for retries
- No retry on 4xx client errors (except 429)
Middleware
Chain
Chains multiple middleware functions together.
func Chain(h http.Handler, middleware ...func(http.Handler) http.Handler) http.Handler
Example:
handler := httputil.Chain(
yourHandler,
httputil.WithLogging,
sessionMiddleware,
authMiddleware,
)
WithLogging
Logs HTTP requests with method, path, status code, and response size.
func WithLogging(next http.Handler) http.Handler
Example:
handler := httputil.WithLogging(yourHandler)
Log Output:
INFO http called method=GET path=/api/users code=200 size=1024
ERROR http called method=POST path=/api/users code=500 size=0
WithSessionContext
Extracts session information from Bearer tokens or cookies and adds it to request context.
func WithSessionContext[T any](
cookieKey CookieKey,
ctxKey ContextKey,
ctxTokenKey ContextKey,
parseSession func(token string) (T, error)
) func(next http.Handler) http.Handler
Parameters:
cookieKey - Cookie name to check for token
ctxKey - Context key to store parsed session
ctxTokenKey - Context key to store raw token (empty string to skip)
parseSession - Function to parse token into session data
Example:
type User struct {
ID string
Role string
}
parseSession := func(token string) (User, error) {
// Parse your token (JWT, database lookup, etc.)
return User{ID: "123", Role: "admin"}, nil
}
middleware := httputil.WithSessionContext(
httputil.CookieKey("auth_token"),
httputil.ContextKey("user"),
httputil.ContextKey("token"), // Store raw token
parseSession,
)
handler := middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if user, ok := r.Context().Value(httputil.ContextKey("user")).(User); ok {
fmt.Fprintf(w, "Hello, %s (Role: %s)", user.ID, user.Role)
} else {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
}
}))
Token Sources (in priority order):
Authorization: Bearer <token> header
- Cookie specified by
cookieKey
Reverse Proxy
ReverseProxy
Creates a simple reverse proxy to another HTTP service.
func ReverseProxy(rawURL string) (http.HandlerFunc, error)
Example:
// Proxy all requests to another server
proxyHandler, err := httputil.ReverseProxy("http://backend:8080")
if err != nil {
log.Fatal(err)
}
http.Handle("/api/", proxyHandler)
DevProxy
Sets up development-mode proxying with exceptions for certain paths.
func DevProxy(mux *http.ServeMux, service http.Handler, isDev bool, proxyAddr string, exceptions []string) error
Parameters:
mux - HTTP mux to configure
service - Handler for your main service
isDev - Whether in development mode
proxyAddr - Address of development server (e.g., frontend dev server)
exceptions - Paths that should go to service instead of proxy
Example:
mux := http.NewServeMux()
serviceHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("API Response"))
})
// In development: proxy UI requests to dev server, API requests to service
err := httputil.DevProxy(
mux,
serviceHandler,
true, // isDev
"http://localhost:3000", // frontend dev server
[]string{"/api", "/auth"}, // exceptions - these go to service
)
// Requests to /api/* -> serviceHandler
// Requests to /* -> proxy to localhost:3000
File Serving
ServeFile
Serves static files from a filesystem with SPA (Single Page Application) fallback.
func ServeFile(fs fs.FS) http.Handler
Example with embedded files:
//go:embed static/*
var staticFiles embed.FS
func main() {
staticFS, _ := fs.Sub(staticFiles, "static")
http.Handle("/", httputil.ServeFile(staticFS))
}
Example with directory:
http.Handle("/static/", httputil.ServeFile(os.DirFS("./public")))
SPA Behavior:
- If requested file exists, serves it normally
- If file doesn't exist, sets path to "/" and serves that (typically index.html)
- Perfect for React/Vue/Angular SPAs with client-side routing
Request Limiting
ReadLimiter
Applies size limit to request body (note: function name has typo but works correctly).
func ReadLimter(size int64, w http.ResponseWriter, r *http.Request)
Example:
func handler(w http.ResponseWriter, r *http.Request) {
// Limit request body to 1MB
httputil.ReadLimter(1024*1024, w, r)
body, err := io.ReadAll(r.Body)
if err != nil {
// Handle size limit exceeded
return
}
// Process body...
}
ReadBodyLimiter
Applies size limit and reads the body in one operation.
func ReadBodyLimiter(size int64, w http.ResponseWriter, r *http.Request) ([]byte, error)
Example:
func apiHandler(w http.ResponseWriter, r *http.Request) {
// Limit and read body (max 10KB)
body, err := httputil.ReadBodyLimiter(10*1024, w, r)
if err != nil {
// ReadBodyLimiter already set 400 status
w.Write([]byte("Request too large"))
return
}
// Parse JSON, etc.
var data map[string]interface{}
json.Unmarshal(body, &data)
// Process data...
}
Error Handling
Cookie Errors
value, err := httputil.GetCookie(key, r)
if err != nil {
if errors.Is(err, httputil.ErrParsingCookie) {
// Handle cookie parsing error
}
}
JWT Errors
err := jwtHandler.Decode(token, claims)
if err != nil {
// Could be: invalid signature, expired token, malformed token, etc.
log.Printf("JWT decode error: %v", err)
}
Retry Client Errors
client, err := httputil.NewRetryClient(
httputil.WithMaxRetries(-1), // Invalid!
)
if err != nil {
// Handle configuration error
}
Complete Example
Here's a complete example showing multiple features working together:
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"time"
"ella.to/httputil"
)
type User struct {
ID string `json:"id"`
Name string `json:"name"`
Role string `json:"role"`
}
type UserClaims struct {
*httputil.JwtRegisteredClaims
UserID string `json:"user_id"`
Role string `json:"role"`
}
func (c *UserClaims) ParseToken(token *httputil.JwtToken) error {
if !token.Valid {
return fmt.Errorf("invalid token")
}
return nil
}
func main() {
// Setup JWT
jwtHandler := httputil.New("super-secret-key")
// Setup middleware
parseSession := func(token string) (User, error) {
claims := &UserClaims{JwtRegisteredClaims: &httputil.JwtRegisteredClaims{}}
err := jwtHandler.Decode(token, claims)
if err != nil {
return User{}, err
}
return User{ID: claims.UserID, Role: claims.Role}, nil
}
sessionMiddleware := httputil.WithSessionContext(
httputil.CookieKey("session"),
httputil.ContextKey("user"),
httputil.ContextKey(""),
parseSession,
)
// Setup routes
mux := http.NewServeMux()
// Login endpoint
mux.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
// Limit request size
body, err := httputil.ReadBodyLimiter(1024, w, r)
if err != nil {
w.Write([]byte("Request too large"))
return
}
var loginReq struct {
Username string `json:"username"`
Password string `json:"password"`
}
if err := json.Unmarshal(body, &loginReq); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
// Authenticate user (simplified)
if loginReq.Username == "admin" && loginReq.Password == "secret" {
claims := &UserClaims{
JwtRegisteredClaims: &httputil.JwtRegisteredClaims{
ExpiresAt: httputil.JwtNewNumericDate(time.Now().Add(24 * time.Hour)),
Subject: loginReq.Username,
},
UserID: "admin123",
Role: "admin",
}
token, err := jwtHandler.Encode(claims)
if err != nil {
http.Error(w, "Token generation failed", http.StatusInternalServerError)
return
}
// Set secure cookie
httputil.SetCookie(w, httputil.CookieKey("session"), token, 24*time.Hour, true)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"token": token})
} else {
http.Error(w, "Invalid credentials", http.StatusUnauthorized)
}
})
// Protected endpoint
mux.HandleFunc("/profile", func(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(httputil.ContextKey("user")).(User)
if !ok {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
})
// Setup development proxy for frontend
err := httputil.DevProxy(
mux,
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "API endpoint not found", http.StatusNotFound)
}),
true, // development mode
"http://localhost:3000", // frontend dev server
[]string{"/api", "/login", "/profile"}, // API exceptions
)
if err != nil {
log.Fatal("DevProxy setup failed:", err)
}
// Chain all middleware
handler := httputil.Chain(
mux,
sessionMiddleware,
httputil.WithLogging,
)
log.Println("Server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", handler))
}
Type Safety
The library uses custom types for better type safety:
type CookieKey string // For cookie names
type ContextKey string // For context keys
This prevents mixing up string parameters and provides better IDE support.
Testing
The library includes comprehensive tests with no mocking - all tests use real HTTP servers and actual functionality. Run tests with:
go test ./...
License
MIT License - see LICENSE.md for details.