A comprehensive HTTP utilities library for Go that provides common HTTP functionality including JWT handling, middleware, proxying, file serving, and more.
- 🍪 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)
go get ella.to/httputilpackage 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)
}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 writerkey- Cookie name (typed as CookieKey for type safety)value- Cookie valuemaxAge- 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)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...func New(secretKey string) *JwtExample:
jwtHandler := httputil.New("your-secret-key")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)func (j *Jwt) Decode(jwt string, claims JwtClaims) errorNote: 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.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)
Chains multiple middleware functions together.
func Chain(h http.Handler, middleware ...func(http.Handler) http.Handler) http.HandlerExample:
handler := httputil.Chain(
yourHandler,
httputil.WithLogging,
sessionMiddleware,
authMiddleware,
)Logs HTTP requests with method, path, status code, and response size.
func WithLogging(next http.Handler) http.HandlerExample:
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
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.HandlerParameters:
cookieKey- Cookie name to check for tokenctxKey- Context key to store parsed sessionctxTokenKey- 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
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)Sets up development-mode proxying with exceptions for certain paths.
func DevProxy(mux *http.ServeMux, service http.Handler, isDev bool, proxyAddr string, exceptions []string) errorParameters:
mux- HTTP mux to configureservice- Handler for your main serviceisDev- Whether in development modeproxyAddr- 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:3000Serves static files from a filesystem with SPA (Single Page Application) fallback.
func ServeFile(fs fs.FS) http.HandlerExample 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
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...
}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...
}value, err := httputil.GetCookie(key, r)
if err != nil {
if errors.Is(err, httputil.ErrParsingCookie) {
// Handle cookie parsing error
}
}err := jwtHandler.Decode(token, claims)
if err != nil {
// Could be: invalid signature, expired token, malformed token, etc.
log.Printf("JWT decode error: %v", err)
}client, err := httputil.NewRetryClient(
httputil.WithMaxRetries(-1), // Invalid!
)
if err != nil {
// Handle configuration error
}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))
}The library uses custom types for better type safety:
type CookieKey string // For cookie names
type ContextKey string // For context keysThis prevents mixing up string parameters and provides better IDE support.
The library includes comprehensive tests with no mocking - all tests use real HTTP servers and actual functionality. Run tests with:
go test ./...MIT License - see LICENSE.md for details.