httputil

package module
v0.0.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 5, 2025 License: Apache-2.0 Imports: 13 Imported by: 1

README

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

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):

  1. Authorization: Bearer <token> header
  2. 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

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.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	JwtParseRSAPrivateKeyFromPEM = jwtgo.ParseRSAPrivateKeyFromPEM
	JwtNewWithClaims             = jwtgo.NewWithClaims
	JwtSigningMethodRS256        = jwtgo.SigningMethodRS256
	JwtNewNumericDate            = jwtgo.NewNumericDate
)
View Source
var (
	ErrParsingCookie = errors.New("parsing cookie")
)

Functions

func Chain

func Chain(h http.Handler, middleware ...func(http.Handler) http.Handler) http.Handler

func DevProxy

func DevProxy(mux *http.ServeMux, service http.Handler, isDev bool, proxyAddr string, exceptions []string) error

DevProxy is for serving static files in development mode. mainly used for UI development with a separate server.

func GetCookie

func GetCookie(key CookieKey, r *http.Request) (string, error)

Get tries to extract cookie by name from request object

func NewRetryClient added in v0.0.4

func NewRetryClient(opts ...retryTransportOpt) (*http.Client, error)

NewRetryClient creates an HTTP client with retry logic and header injection

func ReadBodyLimiter

func ReadBodyLimiter(size int64, w http.ResponseWriter, r *http.Request) ([]byte, error)

func ReadLimter

func ReadLimter(size int64, w http.ResponseWriter, r *http.Request)

func ReverseProxy

func ReverseProxy(rawURL string) (http.HandlerFunc, error)

func ServeFile

func ServeFile(fs fs.FS) http.Handler

func SetCookie

func SetCookie(w http.ResponseWriter, key CookieKey, value string, maxAge time.Duration, secure bool)

Set maxAge is in seconds, if maxAge is zero, it deletes the cookie

func WithHeaders added in v0.0.4

func WithHeaders(headers map[string]string) retryTransportOpt

func WithInitialDelay added in v0.0.4

func WithInitialDelay(delay time.Duration) retryTransportOpt

func WithLogging

func WithLogging(next http.Handler) http.Handler

func WithMaxDelay added in v0.0.4

func WithMaxDelay(delay time.Duration) retryTransportOpt

func WithMaxRetries added in v0.0.4

func WithMaxRetries(maxRetries int) retryTransportOpt

func WithSessionContext

func WithSessionContext[T any](cookieKey CookieKey, ctxKey ContextKey, ctxTokenKey ContextKey, parseSession func(token string) (T, error)) func(next http.Handler) http.Handler

Types

type ContextKey

type ContextKey string

type CookieKey

type CookieKey string

type Jwt

type Jwt struct {
	// contains filtered or unexported fields
}

func New

func New(secretKey string) *Jwt

func (*Jwt) Decode

func (j *Jwt) Decode(jwt string, claims JwtClaims) error

func (*Jwt) Encode

func (j *Jwt) Encode(claims JwtClaims) (string, error)

type JwtClaims

type JwtClaims = jwtgo.Claims

type JwtMapClaims

type JwtMapClaims = jwtgo.MapClaims

type JwtRegisteredClaims

type JwtRegisteredClaims = jwtgo.RegisteredClaims

type JwtToken

type JwtToken = jwtgo.Token

type TokenParser

type TokenParser interface {
	ParseToken(token *JwtToken) error
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL