srv

package
v1.0.41 Latest Latest
Warning

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

Go to latest
Published: Apr 26, 2026 License: MPL-2.0 Imports: 25 Imported by: 0

README

srv - HTTP Server Utilities

The srv package provides comprehensive HTTP server utilities for Go applications, including middleware support, graceful shutdown, HTTP context abstraction, and enhanced routing capabilities.

Features

  • 🛠️ HTTP Context: Convenient request/response wrapper with value storage
  • 📦 Request Parsing: Universal parsing of JSON, form data, and query parameters with struct tags and custom type support
  • 🔀 Enhanced Router: Extended ServeMux with RESTful HTTP method helpers
  • 🔄 URL Reversing: Named routes with automatic URL generation and parameter substitution
  • 🔗 Middleware System: Composable HTTP middleware with easy chaining
  • 📊 Built-in Middleware: Logging, panic recovery, CORS, trailing slash, and session management included
  • 🍪 Session Stores: In-memory and encrypted cookie-based session storage options
  • 🛑 Graceful Shutdown: HTTP server with signal-based graceful shutdown
  • 📝 Structured Logging: Integration with Go's structured logging (log/slog)
  • 🔗 ERM Integration: Uses the erm package for standardized error handling and HTTP status codes
  • ⚡ Minimal Dependencies: Uses Go standard library plus c3p0-box/utils/erm for errors

Installation

go get github.com/c3p0-box/utils/srv

Quick Start

package main

import (
    "crypto/rand"
    "errors"
    "log"
    "github.com/c3p0-box/utils/srv"
    "github.com/c3p0-box/utils/erm"
)

func main() {
    // Create enhanced router with error handling
    mux := srv.NewMux()
    
    // Add encrypted session middleware (for stateless apps)
    key := make([]byte, 32) // AES-256 key
    if _, err := rand.Read(key); err != nil {
        log.Fatal(err)
    }
    store, err := srv.NewCookieStore("app-session", key, srv.NewOptions())
    if err != nil {
        log.Fatal(err)
    }
    mux.Middleware(srv.SessionMiddleware(store, "app-session"))
    
    // Alternative: In-memory session store (for single-instance apps)
    // store := srv.NewInMemoryStore("app-session", srv.NewOptions())
    // defer store.Close()
    // mux.Middleware(srv.SessionMiddleware(store, "app-session"))
    
    // Add named routes for URL generation
    mux.Get("users", "/users", func(ctx srv.Context) error {
        // Generate URL to create user endpoint
        createURL, _ := mux.Reverse("users", nil)
        return ctx.JSON(200, map[string]interface{}{
            "message": "Users list",
            "create_url": createURL,
        })
    })
    
    mux.Post("users", "/users", func(ctx srv.Context) error {
        name := ctx.FormValue("name")
        if name == "" {
            return errors.New("name is required")  // Error handled automatically
        }
        // Generate URL to view the created user
        userURL, _ := mux.Reverse("user", map[string]string{"id": "123"})
        return ctx.JSON(201, map[string]interface{}{
            "created": name,
            "user_url": userURL,
        })
    })
    
    mux.Get("user", "/users/{id}", func(ctx srv.Context) error {
        return ctx.JSON(200, map[string]string{"id": ctx.Param("id")})
    })
    
    // Set custom error handler with erm integration (optional)
    mux.ErrorHandler(func(ctx srv.Context, err error) {
        log.Printf("Handler error: %v", err)
        // erm errors provide proper HTTP status codes
        status := erm.Status(err)  // Extract HTTP status from erm error
        message := erm.Message(err) // Get user-safe message
        ctx.JSON(status, map[string]string{"error": message})
    })
    
    // Add middleware
    mux.Middleware(srv.LoggingMiddleware)
    mux.Middleware(srv.RecoverMiddleware)
    
    // Run server with graceful shutdown
    err := srv.RunServer(mux, "localhost", "8080", func() error {
        log.Println("Cleaning up...")
        return nil
    })
    
    if err != nil {
        log.Fatal(err)
    }
}

API Reference

🛠️ HttpContext - Request/Response Wrapper
Overview

HttpContext provides a convenient wrapper around http.Request and http.ResponseWriter with additional functionality for value storage, parameter handling, and response generation.

Constructor
func NewHttpContext(w http.ResponseWriter, r *http.Request) *HttpContext
Value Store (Thread-Safe)
ctx.Set("user", userObj)           // Store value
user := ctx.Get("user")            // Retrieve value
Request Information
method := ctx.Method()             // HTTP method: "GET", "POST", etc.
path := ctx.Path()                 // URL path: "/api/users"
isTLS := ctx.IsTLS()              // true for HTTPS requests
isWS := ctx.IsWebSocket()         // true for WebSocket upgrades
Parameters & Headers
// Query parameters
name := ctx.QueryParam("name")     // Single query parameter
query := ctx.Query()               // All query parameters

// Path parameters (Go 1.22+ ServeMux)
id := ctx.Param("id")              // Path parameter: /users/{id}

// Form data
username := ctx.FormValue("username")

// Headers
auth := ctx.GetHeader("Authorization")
ctx.SetHeader("Content-Type", "application/json")
ctx.AddHeader("X-Custom", "value")
Cookies
// Read cookies
cookie, err := ctx.Cookie("session")
allCookies := ctx.Cookies()

// Set cookies
ctx.SetCookie(&http.Cookie{
    Name:  "session",
    Value: "abc123",
    Path:  "/",
})
Response Methods
// JSON response (with error handling)
err := ctx.JSON(200, data)

// Text response
err := ctx.String(200, "Hello, World!")

// HTML response
err := ctx.HTML(200, "<h1>Welcome</h1>")

// HTML Blob response
err := ctx.HTMLBlob(200, []byte("<h1>Welcome</h1>"))

// Redirects
ctx.Redirect(302, "/login")

// Custom status code
ctx.WriteHeader(204)
📦 ParseRequest - Universal Request Parsing
Overview

ParseRequest provides unified parsing of HTTP request data, automatically handling both request body content (JSON, form data, multipart forms) and URL query parameters. It uses struct tags to map data to Go structs with proper type conversion and error handling.

Function Signature
func ParseRequest(r *http.Request, target interface{}) erm.Error
Supported Features

Content Types:

  • application/json - JSON payload parsing
  • application/x-www-form-urlencoded - HTML form data
  • multipart/form-data - File uploads and form data
  • Query parameters (always parsed regardless of Content-Type)

Struct Tags:

  • json:"field_name" - Maps JSON fields
  • form:"field_name" - Maps form data fields
  • query:"field_name" - Maps URL query parameters

Supported Types:

  • string, int, int8, int16, int32, int64
  • uint, uint8, uint16, uint32, uint64
  • float32, float64, bool
  • Custom types implementing encoding.TextUnmarshaler interface
Basic Usage
// Custom type example
type UserID [16]byte

func (u *UserID) UnmarshalText(text []byte) error {
    if len(text) != 32 {
        return fmt.Errorf("invalid UserID length")
    }
    _, err := hex.Decode(u[:], text)
    return err
}

type UserRequest struct {
    // JSON/Form body fields
    Name     string  `json:"name" form:"name"`
    Email    string  `json:"email" form:"email"`
    Age      int     `json:"age" form:"age"`
    Active   bool    `json:"active" form:"active"`
    
    // Query parameters (including custom types)
    ID       UserID  `query:"user_id"`    // Custom type with TextUnmarshaler
    Page     int     `query:"page"`
    Sort     string  `query:"sort"`
    Limit    int     `query:"limit"`
    Debug    bool    `query:"debug"`
}

// Handler function
func createUser(ctx *srv.HttpContext) error {
    var req UserRequest
    if err := srv.ParseRequest(ctx.Request(), &req); err != nil {
        return err  // Error handled automatically
    }
    
    // Use both body data and query parameters
    log.Printf("Creating user: %s (page=%d, sort=%s)", req.Name, req.Page, req.Sort)
    
    // Process the request...
    return ctx.JSON(201, map[string]interface{}{
        "user":  req.Name,
        "debug": req.Debug,
    })
}
JSON Request with Query Parameters
// POST /users?page=2&sort=name&debug=true
// Content-Type: application/json
// Body: {"name": "John", "email": "john@example.com", "age": 30}

type CreateUserRequest struct {
    // From JSON body
    Name  string `json:"name"`
    Email string `json:"email"`
    Age   int    `json:"age"`
    
    // From query parameters
    Page  int    `query:"page"`
    Sort  string `query:"sort"`
    Debug bool   `query:"debug"`
}

func handler(ctx *srv.HttpContext) error {
    var req CreateUserRequest
    if err := srv.ParseRequest(ctx.Request(), &req); err != nil {
        return err
    }
    
    // req.Name = "John" (from JSON)
    // req.Page = 2 (from query)
    // req.Debug = true (from query)
    
    return ctx.JSON(200, req)
}
Form Data with Query Parameters
// POST /users?page=1&sort=email
// Content-Type: application/x-www-form-urlencoded
// Body: name=Jane&email=jane@example.com&active=true

type FormUserRequest struct {
    // From form data
    Name   string `form:"name"`
    Email  string `form:"email"`
    Active bool   `form:"active"`
    
    // From query parameters
    Page   int    `query:"page"`
    Sort   string `query:"sort"`
}

func handler(ctx *srv.HttpContext) error {
    var req FormUserRequest
    if err := srv.ParseRequest(ctx.Request(), &req); err != nil {
        return err
    }
    
    // req.Name = "Jane" (from form)
    // req.Page = 1 (from query)
    
    return ctx.JSON(200, req)
}
Multipart Form with File Upload
type FileUploadRequest struct {
    // From multipart form
    Title       string `form:"title"`
    Description string `form:"description"`
    
    // From query parameters
    Folder string `query:"folder"`
    Public bool   `query:"public"`
}

func uploadHandler(ctx *srv.HttpContext) error {
    var req FileUploadRequest
    if err := srv.ParseRequest(ctx.Request(), &req); err != nil {
        return err
    }
    
    // Handle file upload
    file, header, err := ctx.Request().FormFile("file")
    if err != nil {
        return err
    }
    defer file.Close()
    
    // Use form data and query parameters
    log.Printf("Uploading %s to folder %s (public=%v)", 
        header.Filename, req.Folder, req.Public)
    
    return ctx.JSON(200, map[string]string{
        "title":    req.Title,
        "filename": header.Filename,
        "folder":   req.Folder,
    })
}
Custom Types with TextUnmarshaler

ParseRequest supports custom types that implement the encoding.TextUnmarshaler interface, allowing you to parse complex data types from form and query parameters.

import (
    "encoding"
    "encoding/hex"
    "fmt"
    "time"
)

// Custom 16-byte user identifier
type UserID [16]byte

func (u *UserID) UnmarshalText(text []byte) error {
    if len(text) != 32 { // hex encoded 16 bytes = 32 characters
        return fmt.Errorf("invalid UserID length: expected 32, got %d", len(text))
    }
    _, err := hex.Decode(u[:], text)
    return err
}

func (u UserID) String() string {
    return hex.EncodeToString(u[:])
}

// Custom time format
type CustomDate time.Time

func (cd *CustomDate) UnmarshalText(text []byte) error {
    t, err := time.Parse("2006-01-02", string(text))
    if err != nil {
        return err
    }
    *cd = CustomDate(t)
    return nil
}

func (cd CustomDate) String() string {
    return time.Time(cd).Format("2006-01-02")
}

// Custom enum-like type
type Priority int

const (
    PriorityLow Priority = iota
    PriorityMedium
    PriorityHigh
)

func (p *Priority) UnmarshalText(text []byte) error {
    switch string(text) {
    case "low":
        *p = PriorityLow
    case "medium":
        *p = PriorityMedium
    case "high":
        *p = PriorityHigh
    default:
        return fmt.Errorf("invalid priority: %s", string(text))
    }
    return nil
}

// Usage in request struct
type TaskRequest struct {
    // Custom types in form/query parameters
    UserID      UserID     `form:"user_id" query:"user_id"`
    DueDate     CustomDate `form:"due_date" query:"due_date"`
    Priority    Priority   `form:"priority" query:"priority"`
    
    // Pointer versions (automatically handled)
    AssigneeID  *UserID     `form:"assignee_id" query:"assignee_id"`
    StartDate   *CustomDate `form:"start_date" query:"start_date"`
    
    // Regular fields
    Title       string `json:"title" form:"title"`
    Description string `json:"description" form:"description"`
}

func createTaskHandler(ctx *srv.HttpContext) error {
    var req TaskRequest
    if err := srv.ParseRequest(ctx.Request(), &req); err != nil {
        return err // Automatic error handling for invalid formats
    }
    
    // All custom types are automatically parsed
    log.Printf("Creating task for user %s, due %s, priority %d", 
        req.UserID.String(), req.DueDate.String(), req.Priority)
    
    return ctx.JSON(201, map[string]interface{}{
        "message":    "Task created successfully",
        "user_id":    req.UserID.String(),
        "due_date":   req.DueDate.String(),
        "priority":   req.Priority,
        "assignee":   req.AssigneeID, // nil if not provided
    })
}

Example Requests:

Form Data:

POST /tasks
Content-Type: application/x-www-form-urlencoded

user_id=0123456789abcdef0123456789abcdef&due_date=2024-12-25&priority=high&title=Complete+project

Query Parameters:

GET /tasks?user_id=fedcba9876543210fedcba9876543210&due_date=2024-01-15&priority=medium

Multipart Form:

POST /tasks
Content-Type: multipart/form-data

user_id: abcdef0123456789abcdef0123456789
due_date: 2024-06-30
priority: low
title: Review documentation
description: Review and update project documentation

JSON with Query Parameters:

POST /tasks?user_id=1234567890abcdef1234567890abcdef&priority=high
Content-Type: application/json

{
  "title": "Fix bug #123",
  "description": "Critical security fix"
}

Error Handling:

Custom types can return specific errors that are automatically handled:

type ProductCode string

func (pc *ProductCode) UnmarshalText(text []byte) error {
    s := string(text)
    if len(s) < 3 {
        return fmt.Errorf("product code too short: minimum 3 characters")
    }
    if !strings.HasPrefix(s, "PRD-") {
        return fmt.Errorf("product code must start with 'PRD-'")
    }
    *pc = ProductCode(s)
    return nil
}

// Invalid product codes in form/query will return 400 Bad Request
Advanced Tag Usage
type AdvancedRequest struct {
    // Custom field names
    UserID    int    `query:"user_id"`
    SearchTerm string `query:"q"`
    
    // Skip fields
    Password string `json:"password" form:"password"`
    Internal string `query:"-"`  // Never parsed from query
    
    // Fields without tags use lowercase field name
    Page  int     // Uses "page" as query parameter name
    Limit int     // Uses "limit" as query parameter name
}
Query-Only Requests (GET)
// GET /search?q=golang&page=2&limit=20&sort=date&active=true

type SearchRequest struct {
    Query  string `query:"q"`
    Page   int    `query:"page"`
    Limit  int    `query:"limit"`
    Sort   string `query:"sort"`
    Active bool   `query:"active"`
}

func searchHandler(ctx *srv.HttpContext) error {
    var req SearchRequest
    if err := srv.ParseRequest(ctx.Request(), &req); err != nil {
        return err
    }
    
    // All fields populated from query parameters
    results := performSearch(req.Query, req.Page, req.Limit)
    
    return ctx.JSON(200, map[string]interface{}{
        "results": results,
        "query":   req.Query,
        "page":    req.Page,
    })
}
Error Handling
func handlerWithValidation(ctx *srv.HttpContext) error {
    var req UserRequest
    if err := srv.ParseRequest(ctx.Request(), &req); err != nil {
        // Common errors:
        // - Invalid JSON format
        // - Type conversion errors (e.g., "abc" -> int)
        // - Malformed Content-Type header
        return err  // Returns 400 Bad Request with details
    }
    
    // Additional validation
    if req.Name == "" {
        return erm.BadRequest("Name is required", nil)
    }
    if req.Age < 0 {
        return erm.BadRequest("Age must be positive", nil)
    }
    
    return ctx.JSON(200, req)
}
Type Conversion Examples
// URL: /api/users?page=5&active=true&score=95.5&tags=&user_id=1234567890abcdef1234567890abcdef

type TypeExampleRequest struct {
    Page    int     `query:"page"`    // "5" -> 5
    Active  bool    `query:"active"`  // "true" -> true
    Score   float64 `query:"score"`   // "95.5" -> 95.5
    Tags    string  `query:"tags"`    // "" -> "" (empty string)
    UserID  UserID  `query:"user_id"` // "1234567890abcdef1234567890abcdef" -> UserID via UnmarshalText
    Missing int     `query:"missing"` // (not provided) -> 0 (zero value)
}
Best Practices
  1. Combine Tags: Use multiple tags for flexibility
type FlexibleRequest struct {
    Name string `json:"name" form:"name" query:"name"`
}
  1. Validation: Always validate parsed data
if req.Page < 1 {
    req.Page = 1  // Set default
}
  1. Error Handling: Return meaningful errors
if err := srv.ParseRequest(ctx.Request(), &req); err != nil {
    return erm.BadRequest("Invalid request format", err)
}
  1. Custom Types: Implement TextUnmarshaler for complex parsing
type ProductID string

func (p *ProductID) UnmarshalText(text []byte) error {
    s := string(text)
    if !strings.HasPrefix(s, "prod_") {
        return fmt.Errorf("invalid product ID format")
    }
    *p = ProductID(s)
    return nil
}
  1. Documentation: Document expected parameters
// UserListRequest handles GET /users?page=N&sort=field&limit=N
type UserListRequest struct {
    Page  int    `query:"page"`  // Page number (default: 1)
    Sort  string `query:"sort"`  // Sort field: "name", "email", "created"
    Limit int    `query:"limit"` // Items per page (default: 20, max: 100)
}
🔀 Mux - Enhanced HTTP Router
Overview

Mux wraps Go's standard http.ServeMux with convenient HTTP method helpers, RESTful routing support, and centralized error handling.

HandlerFunc Type
type HandlerFunc func(ctx *HttpContext) error

The new HandlerFunc signature provides several advantages:

  • Automatic HttpContext: No need to manually create HttpContext
  • Error Handling: Return errors instead of writing error responses manually
  • Cleaner Code: Less boilerplate, more focused business logic
Constructor
mux := srv.NewMux()  // Includes default error handler
HTTP Method Helpers with Optional Naming
// Named routes for URL generation
mux.Get("user-list", "/users", func(ctx *HttpContext) error {
    return ctx.JSON(200, users)
})

mux.Post("user-create", "/users", func(ctx *HttpContext) error {
    user := parseUser(ctx)
    if err := validateUser(user); err != nil {
        return err  // Automatically handled by error handler
    }
    return ctx.JSON(201, user)
})

// Unnamed routes (empty string name)
mux.Get("", "/health", func(ctx *HttpContext) error {
    return ctx.String(200, "OK")
})

// Same name for different methods (RESTful pattern)
mux.Get("users", "/users", listUsersHandler)      // GET /users
mux.Post("users", "/users", createUserHandler)    // POST /users
mux.Get("user", "/users/{id}", getUserHandler)    // GET /users/{id}
mux.Put("user", "/users/{id}", updateUserHandler) // PUT /users/{id}
mux.Delete("user", "/users/{id}", deleteUserHandler) // DELETE /users/{id}

// All HTTP methods supported
mux.Patch("user-patch", "/users/{id}", handler)  // PATCH requests
mux.Head("health-check", "/ping", handler)       // HEAD requests
mux.Options("cors", "/api/*", handler)           // OPTIONS requests
URL Reversing (Simplified)
// Generate URLs from route names (no method parameter needed)
usersURL, err := mux.Reverse("users", nil)
// Returns: "/users" (works for both GET and POST since same pattern)

userURL, err := mux.Reverse("user", map[string]string{"id": "123"})
// Returns: "/users/123" (works for GET, PUT, DELETE since same pattern)

// Error handling with erm package integration
userURL, err := mux.Reverse("non-existent", nil)
// Returns: "", erm.NotFound error with 404 status

userURL, err := mux.Reverse("user-profile", nil)  // Missing {id} parameter
// Returns: "", erm.RequiredError for missing parameters
URL Generation in Handlers
mux.Get("users", "/users", func(ctx *HttpContext) error {
    // Generate URLs to related endpoints
    createURL, _ := mux.Reverse("users", nil)  // Simplified: no method needed
    
    return ctx.JSON(200, map[string]interface{}{
        "users": getUsersList(),
        "links": map[string]string{
            "create": createURL,
        },
    })
})

mux.Get("user", "/users/{id}", func(ctx *HttpContext) error {
    id := ctx.Param("id")
    
    // Generate URLs with dynamic parameters (no method needed)
    editURL, _ := mux.Reverse("user", map[string]string{"id": id})
    
    return ctx.JSON(200, map[string]interface{}{
        "user": getUser(id),
        "links": map[string]string{
            "edit": editURL,  // Same URL for PUT, DELETE since same pattern
        },
    })
})
Error Handling
// Custom error handler (optional)
mux.ErrorHandler(func(ctx *HttpContext, err error) {
    log.Printf("Handler error: %v", err)
    
    // Handle different error types
    switch e := err.(type) {
    case *ValidationError:
        ctx.JSON(400, map[string]string{"error": e.Error()})
    case *NotFoundError:
        ctx.JSON(404, map[string]string{"error": "Not found"})
    default:
        ctx.JSON(500, map[string]string{"error": "Internal server error"})
    }
})

// Default error handler returns 500 with generic message
Standard ServeMux Methods (Traditional Handlers)
mux.Handle("/api/", apiHandler)         // Register http.Handler
mux.HandleFunc("/health", healthFunc)   // Register http.HandlerFunc
Access Underlying ServeMux
stdMux := mux.Mux()  // Get *http.ServeMux for advanced usage
🔗 Middleware System

The srv package uses HandlerFunc-based middleware that works seamlessly with the Context interface and error handling system.

Built-in Middleware

Logging Middleware

mux.Middleware(srv.LoggingMiddleware)  // Structured logging with slog

Captures: method, path, user agent, remote address, and processing duration

Recovery Middleware

mux.Middleware(srv.RecoverMiddleware)  // Panic recovery with error conversion

Converts panics to errors that are handled by the error handler

CORS Middleware

// Default CORS configuration (allows all origins)
mux.Middleware(srv.CORSMiddleware(srv.DefaultCORSConfig))

// Production CORS configuration with security best practices
corsConfig := srv.CORSConfig{
    AllowOrigins:     []string{"https://app.example.com", "https://admin.example.com"},
    AllowMethods:     []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
    AllowHeaders:     []string{"Content-Type", "Authorization", "X-API-Key"},
    AllowCredentials: true,
    ExposeHeaders:    []string{"X-Total-Count", "X-Rate-Limit", "X-Page-Count"},
    MaxAge:           3600, // Cache preflight responses for 1 hour
}
mux.Middleware(srv.CORSMiddleware(corsConfig))

// Note: For preflight requests, register OPTIONS handlers explicitly
mux.Options("", "/api/*", func(ctx Context) error { return nil })

Handles Cross-Origin Resource Sharing with W3C-compliant preflight support, origin validation, credential handling, expose headers, and security hardening. Includes automatic Vary header management and protection against common CORS misconfigurations.

Trailing Slash Middleware

// Default configuration (internal forward)
mux.Middleware(srv.AddTrailingSlashMiddleware(srv.DefaultTrailingSlashConfig))

// Redirect configuration
redirectConfig := srv.TrailingSlashConfig{RedirectCode: 301}
mux.Middleware(srv.AddTrailingSlashMiddleware(redirectConfig))

Adds trailing slashes to URLs for consistency and SEO. Can either redirect or forward internally

Session Middleware

// Create in-memory session store
store := srv.NewInMemoryStore("app-session", srv.NewOptions())
defer store.Close()  // Important: cleanup store on shutdown

// Add session middleware
mux.Middleware(srv.SessionMiddleware(store, "app-session"))

// Use sessions in handlers
mux.Get("profile", "/profile", func(ctx srv.Context) error {
    session := ctx.Get("session").(*srv.Session)
    userID := session.Get("userID")
    if userID == "" {
        return ctx.Redirect(302, "/login")
    }
    return ctx.JSON(200, map[string]interface{}{"userID": userID})
})

mux.Post("login", "/login", func(ctx srv.Context) error {
    session := ctx.Get("session").(*srv.Session)
    session.Set("userID", "12345")
    session.Set("username", "john_doe")
    return ctx.JSON(200, map[string]string{"status": "logged in"})
})

Session features include:

  • Thread-safe in-memory storage with automatic cleanup of expired sessions
  • Configurable cookie options (Path, Domain, MaxAge, Secure, HttpOnly, SameSite)
  • Cryptographically secure session IDs generated with crypto/rand
  • Context integration for easy session access in handlers
  • Store interface for custom session storage backends (Redis, database, etc.)

Custom session configuration:

// Custom session options
options := &srv.Options{
    Path:     "/api",                    // Cookie path
    Domain:   "example.com",             // Cookie domain
    MaxAge:   3600,                      // Session timeout (seconds)
    Secure:   true,                      // HTTPS only
    HttpOnly: true,                      // Prevent XSS
    SameSite: http.SameSiteStrictMode,   // CSRF protection
}
store := srv.NewInMemoryStore("secure-session", options)

// Use with middleware
mux.Middleware(srv.SessionMiddleware(store, "secure-session"))

// Custom store implementation (example)
type RedisStore struct {
    client *redis.Client
    options *srv.Options
}

func (r *RedisStore) Get(req *http.Request, name string) (*srv.Session, error) {
    // Custom Redis implementation
}

Cookie Store (Encrypted Client-Side Storage)

For stateless applications or when you want to avoid server-side session storage, use the CookieStore that encrypts all session data and stores it directly in cookies:

// Generate a secure encryption key (32 bytes for AES-256)
key := make([]byte, 32)
if _, err := rand.Read(key); err != nil {
    log.Fatal(err)
}

// Create encrypted cookie store
store, err := srv.NewCookieStore("secure-session", key, &srv.Options{
    Path:     "/",
    MaxAge:   3600,                      // 1 hour session timeout
    Secure:   true,                      // HTTPS only in production
    HttpOnly: true,                      // Prevent XSS attacks
    SameSite: http.SameSiteStrictMode,   // CSRF protection
})
if err != nil {
    log.Fatal(err)
}

// Use with session middleware (no defer Close() needed for cookie store)
mux.Middleware(srv.SessionMiddleware(store, "secure-session"))

// Session usage remains the same
mux.Post("login", "/login", func(ctx srv.Context) error {
    session := ctx.Get("session").(*srv.Session)
    
    // Authenticate user...
    if validCredentials {
        session.Set("userID", 12345)
        session.Set("role", "admin")
        session.Set("loginTime", time.Now())
        return ctx.JSON(200, map[string]string{"status": "logged in"})
    }
    
    return ctx.JSON(401, map[string]string{"error": "invalid credentials"})
})

CookieStore Features:

  • 🔒 AES-GCM Encryption: Authenticated encryption ensures data confidentiality and integrity
  • 🗃️ Client-side Storage: No server-side session storage required (stateless)
  • 🔑 Configurable Keys: Supports AES-128, AES-192, or AES-256 (16, 24, or 32-byte keys)
  • 📏 Size Monitoring: Automatic validation of cookie size limits (~4KB)
  • 🧮 Gob Serialization: Supports complex Go data types (maps, slices, structs)
  • ⚡ Thread-safe: Safe for concurrent use across multiple requests

Security Considerations:

  • Store encryption keys securely (use environment variables, key management systems)
  • Rotate encryption keys periodically
  • Use different keys for different environments (dev, staging, prod)
  • Set appropriate cookie security options (Secure, HttpOnly, SameSite)
  • Be mindful of session data size (cookie limit ~4KB)

Key Management Example:

// Production key management
func getEncryptionKey() []byte {
    keyStr := os.Getenv("SESSION_ENCRYPTION_KEY")
    if keyStr == "" {
        log.Fatal("SESSION_ENCRYPTION_KEY environment variable required")
    }
    
    key, err := base64.StdEncoding.DecodeString(keyStr)
    if err != nil {
        log.Fatal("Invalid SESSION_ENCRYPTION_KEY format")
    }
    
    if len(key) != 32 { // Require AES-256
        log.Fatal("SESSION_ENCRYPTION_KEY must be 32 bytes for AES-256")
    }
    
    return key
}

// Generate key for new deployment
func generateKey() {
    key := make([]byte, 32)
    if _, err := rand.Read(key); err != nil {
        log.Fatal(err)
    }
    fmt.Printf("SESSION_ENCRYPTION_KEY=%s\n", base64.StdEncoding.EncodeToString(key))
}

Store Comparison:

Feature InMemoryStore CookieStore
Storage Location Server memory Client cookies
Scalability Single instance Stateless/multi-instance
Data Size Limit Memory available ~4KB per session
Persistence Lost on restart Survives restarts
Security Server-side only Encrypted client-side
Performance Fast lookup Encryption overhead
Use Cases Single-server apps Load-balanced/stateless apps

Session management provides:

  • Automatic cookie handling - Sessions are loaded and saved automatically
  • Error resilience - Failed session operations don't crash handlers
  • Memory efficiency - Expired sessions are automatically cleaned up
  • Security - Uses secure defaults and cryptographic session IDs
Middleware Chaining
// Add multiple middleware (applied in order added - first added = outermost wrapper)
mux.Middleware(srv.LoggingMiddleware)                                    // Outermost: logs all requests
mux.Middleware(srv.RecoverMiddleware)                                    // Recovers from panics
mux.Middleware(srv.AddTrailingSlashMiddleware(srv.DefaultTrailingSlashConfig)) // URL normalization
mux.Middleware(srv.CORSMiddleware(srv.DefaultCORSConfig))               // CORS headers
mux.Middleware(srv.SessionMiddleware(store, "app-session"))             // Session management
// Add your custom middleware...
mux.Middleware(func(next srv.HandlerFunc) srv.HandlerFunc {
    return func(ctx srv.Context) error {
        // Custom middleware logic here
        return next(ctx)
    }
})
🛑 RunServer - Graceful Server
Function Signature
func RunServer(handler http.Handler, host string, port string, cleanup func() error) error
Parameters
  • handler: HTTP handler (can be Mux, middleware chain, etc.)
  • host: Host to bind to (defaults to "0.0.0.0" if empty)
  • port: Port to listen on (defaults to "8000" if empty)
  • cleanup: Function called during shutdown for resource cleanup
Graceful Shutdown Process
  1. Listens for SIGINT/SIGTERM signals
  2. Stops accepting new requests
  3. Waits up to 10 seconds for existing requests to complete
  4. Calls the cleanup function
  5. Returns any errors that occurred

Usage Examples

Basic RESTful API
mux := srv.NewMux()

// Set custom error handler for better error responses
mux.ErrorHandler(func(ctx *srv.HttpContext, err error) {
    log.Printf("API Error: %v", err)
    ctx.JSON(500, map[string]string{"error": err.Error()})
})

// Users API with enhanced naming and URL generation
mux.Get("users", "/users", listUsers)
mux.Post("users", "/users", createUser)
mux.Get("user", "/users/{id}", getUser)
mux.Put("user", "/users/{id}", updateUser)
mux.Delete("user", "/users/{id}", deleteUser)

func getUser(ctx *srv.HttpContext) error {
    // Get path parameter
    id := ctx.Param("id")
    
    // Store in context for other middleware
    ctx.Set("userID", id)
    
    // Get user with error handling
    user, err := getUserByID(id)
    if err != nil {
        return err  // Automatically handled by error handler
    }
    
    // Generate URLs to related endpoints
    editURL, _ := mux.Reverse("user", map[string]string{"id": id})
    
    // Return JSON response with links
    return ctx.JSON(200, map[string]interface{}{
        "user": user,
        "links": map[string]string{
            "edit": editURL,
        },
    })
}

func createUser(ctx *srv.HttpContext) error {
    var user User
    if err := json.NewDecoder(ctx.Request().Body).Decode(&user); err != nil {
        return fmt.Errorf("invalid JSON: %w", err)
    }
    
    if err := validateUser(user); err != nil {
        return err
    }
    
    createdUser, err := saveUser(user)
    if err != nil {
        return fmt.Errorf("failed to save user: %w", err)
    }
    
    return ctx.JSON(201, createdUser)
}

// Start server
srv.RunServer(mux, "localhost", "8080", func() error {
    return db.Close()
})
Advanced Middleware Integration
// Create mux with comprehensive middleware stack
mux := srv.NewMux()

// Add session middleware first
store := srv.NewInMemoryStore("app-session", srv.NewOptions())
defer store.Close()
mux.Middleware(srv.SessionMiddleware(store, "app-session"))

// Add HandlerFunc middleware (recommended)
mux.Middleware(srv.LoggingMiddleware)    // Structured logging
mux.Middleware(srv.RecoverMiddleware)    // Panic recovery

// Add custom authentication middleware
mux.Middleware(func(next srv.HandlerFunc) srv.HandlerFunc {
    return func(ctx srv.Context) error {
        token := ctx.GetHeader("Authorization")
        if token == "" {
            return erm.Unauthorized("missing authorization", nil)
        }
        
        // Validate token and store user
        user, err := validateToken(token)
        if err != nil {
            return erm.Unauthorized("invalid token", err)
        }
        ctx.Set("user", user)
        
        return next(ctx)
    }
})

// Add CORS middleware
mux.Middleware(srv.CORSMiddleware(srv.CORSConfig{
    AllowOrigins:     []string{"https://example.com"},
    AllowCredentials: true,
    ExposeHeaders:    []string{"X-Total-Count"},
}))

// Register routes (middleware automatically applied)
mux.Get("users", "/users", listUsersHandler)
mux.Post("users", "/users", createUserHandler)

// All middleware is now configured and will be applied to all routes registered afterward
Enhanced HandlerFunc with Middleware and Error Handling
// Create mux and add middleware
mux := srv.NewMux()

// Add authentication middleware
mux.Middleware(func(next srv.HandlerFunc) srv.HandlerFunc {
    return func(ctx srv.Context) error {
        token := ctx.GetHeader("Authorization")
        if token == "" {
            return erm.Unauthorized("missing authorization", nil)
        }
        // Store authenticated user in context
        ctx.Set("user", getUserFromToken(token))
        return next(ctx)
    }
})

// Add logging middleware
mux.Middleware(srv.LoggingMiddleware)

// Register handler with automatic Context and error handling
mux.Post("user-create", "/users", createUserHandler)

func createUserHandler(ctx srv.Context) error {
	// Parse form data (Context provided automatically)
	name := ctx.FormValue("name")
	email := ctx.FormValue("email")
	
	// Validate input - return error instead of manual response
	if name == "" || email == "" {
		return erm.BadRequest("name and email are required", nil)
	}
	
	// Get authenticated user from middleware context
	authUser, ok := ctx.Get("user").(*User)
	if !ok {
		return erm.Unauthorized("authentication required", nil)
	}
	
	// Create user
	user := &User{
		Name:      name,
		Email:     email,
		CreatedBy: authUser.ID,
	}
	
	// Database operation with error handling
	if err := saveUser(user); err != nil {
		return fmt.Errorf("failed to create user: %w", err)
	}
	
	// Generate URL to view the created user
	userURL, _ := mux.Reverse("user", map[string]string{"id": user.ID})
	
	// Return created user with links (errors handled automatically)
	return ctx.JSON(201, map[string]interface{}{
		"user": user,
		"links": map[string]string{"self": userURL},
	})
}

// Error handling patterns
func advancedErrorHandling(ctx *srv.HttpContext) error {
    // Custom error types for different HTTP status codes
    user, err := validateAndGetUser(ctx)
    if err != nil {
        switch {
        case errors.Is(err, ErrUserNotFound):
            return &HTTPError{Code: 404, Message: "User not found"}
        case errors.Is(err, ErrValidation):
            return &HTTPError{Code: 400, Message: err.Error()}
        default:
            return err  // 500 from default error handler
        }
    }
    
    return ctx.JSON(200, user)
}

// Custom error type for specific HTTP responses
type HTTPError struct {
    Code    int
    Message string
}

func (e *HTTPError) Error() string {
    return e.Message
}

// Custom error handler that understands HTTPError
mux.ErrorHandler(func(ctx *srv.HttpContext, err error) {
    if httpErr, ok := err.(*HTTPError); ok {
        ctx.JSON(httpErr.Code, map[string]string{"error": httpErr.Message})
        return
    }
    
    // Default error handling
    log.Printf("Unexpected error: %v", err)
    ctx.JSON(500, map[string]string{"error": "Internal server error"})
})
File Upload Handling
mux.Post("/upload", func(ctx *srv.HttpContext) error {
    // Parse multipart form (32MB max)
    if err := ctx.Request().ParseMultipartForm(32 << 20); err != nil {
        return fmt.Errorf("invalid form: %w", err)
    }
    
    // Get uploaded file
    file, header, err := ctx.Request().FormFile("file")
    if err != nil {
        return errors.New("no file uploaded")
    }
    defer file.Close()
    
    // Validate file type
    if !isValidFileType(header.Filename) {
        return errors.New("invalid file type")
    }
    
    // Process file with error handling
    savedPath, err := saveUploadedFile(file, header.Filename)
    if err != nil {
        return fmt.Errorf("failed to save file: %w", err)
    }
    
    // Return success response
    return ctx.JSON(200, map[string]string{
        "message":  "File uploaded successfully",
        "filename": header.Filename,
        "path":     savedPath,
    })
})

// Helper functions with proper error handling
func isValidFileType(filename string) bool {
    validTypes := []string{".jpg", ".jpeg", ".png", ".pdf", ".txt"}
    ext := strings.ToLower(filepath.Ext(filename))
    for _, validType := range validTypes {
        if ext == validType {
            return true
        }
    }
    return false
}

func saveUploadedFile(file multipart.File, filename string) (string, error) {
    // Create unique filename
    uniqueName := fmt.Sprintf("%d_%s", time.Now().Unix(), filename)
    destPath := filepath.Join("/uploads", uniqueName)
    
    // Create destination file
    dest, err := os.Create(destPath)
    if err != nil {
        return "", err
    }
    defer dest.Close()
    
    // Copy file content
    if _, err := io.Copy(dest, file); err != nil {
        return "", err
    }
    
    return destPath, nil
}

Testing

The package includes comprehensive tests covering all functionality:

# Run tests
go test ./srv

# Run tests with coverage
go test -cover ./srv

# Run tests with verbose output
go test -v ./srv

# Run benchmarks
go test -bench=. ./srv
Test Coverage
  • HttpContext: Value store, request/response methods, thread safety
  • Mux: HTTP method helpers, routing, integration tests
  • URL Reversing: Named routes, parameter substitution, edge cases, integration
  • Middleware: Type safety, chaining, logging, recovery, CORS, trailing slash
  • RunServer: Parameter validation, error handling, cleanup
  • Integration: Cross-component functionality tests
  • Benchmarks: Performance testing for all components

Current Coverage: 94.2% of statements

Performance

Benchmark results on Apple M1 Max:

BenchmarkHttpContext_ValueStore     99.00 ns/op    21 B/op    2 allocs/op
BenchmarkHttpContext_JSON          938.8 ns/op  1280 B/op   17 allocs/op  
BenchmarkMux_RouteMatching         271.8 ns/op   224 B/op    5 allocs/op

Best Practices

1. Middleware Order

Place middleware in logical order - logging first, authentication/authorization before business logic:

mux.Middleware(srv.LoggingMiddleware)                                    // First: log everything
mux.Middleware(srv.RecoverMiddleware)                                    // Second: catch panics
mux.Middleware(srv.AddTrailingSlashMiddleware(srv.DefaultTrailingSlashConfig)) // Third: URL normalization
mux.Middleware(srv.CORSMiddleware(srv.DefaultCORSConfig))               // Fourth: CORS headers
mux.Middleware(AuthenticationMiddleware)                                  // Fifth: auth before business logic
mux.Middleware(RateLimitingMiddleware)                                    // Last: rate limiting
2. Context Usage

Use HttpContext consistently for request/response operations:

func handler(w http.ResponseWriter, r *http.Request) {
    ctx := srv.NewHttpContext(w, r)
    // Use ctx for all operations instead of w/r directly
}
3. Error Handling

Always handle errors from response methods:

if err := ctx.JSON(200, data); err != nil {
    log.Printf("Failed to write JSON response: %v", err)
}
4. CORS Configuration

Configure CORS appropriately for your security requirements:

// Development - permissive CORS (use only for development)
mux.Middleware(srv.CORSMiddleware(srv.DefaultCORSConfig))

// Production - restrictive CORS with security best practices
prodCorsConfig := srv.CORSConfig{
    // Specify exact origins - avoid wildcards in production
    AllowOrigins:     []string{"https://yourdomain.com", "https://app.yourdomain.com"},
    
    // Include OPTIONS for preflight support
    AllowMethods:     []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
    
    // Explicitly list allowed headers
    AllowHeaders:     []string{"Content-Type", "Authorization", "X-API-Key"},
    
    // Enable credentials for authenticated requests
    AllowCredentials: true,
    
    // Expose custom headers to clients
    ExposeHeaders:    []string{"X-Total-Count", "X-Rate-Limit", "Link"},
    
    // Cache preflight responses for better performance
    MaxAge:           3600,
}
mux.Middleware(srv.CORSMiddleware(prodCorsConfig))

// Register OPTIONS handlers for preflight requests
mux.Options("", "/api/users", func(ctx Context) error { return nil })
mux.Options("", "/api/users/{id}", func(ctx Context) error { return nil })

Security Guidelines:

  • Never use ["*"] for AllowOrigins with AllowCredentials: true
  • Use specific origins instead of wildcards in production
  • Only expose headers that clients actually need via ExposeHeaders
  • Set appropriate MaxAge to balance performance and security
  • Always validate that your frontend origins match exactly
5. Trailing Slash Configuration

Configure trailing slash behavior based on your needs:

// Default - internal forward (no redirect)
mux.Middleware(srv.AddTrailingSlashMiddleware(srv.DefaultTrailingSlashConfig))

// SEO-friendly permanent redirect
seoConfig := srv.TrailingSlashConfig{RedirectCode: 301}
mux.Middleware(srv.AddTrailingSlashMiddleware(seoConfig))

// Temporary redirect for testing
testConfig := srv.TrailingSlashConfig{RedirectCode: 302}
mux.Middleware(srv.AddTrailingSlashMiddleware(testConfig))
6. Resource Cleanup

Always provide cleanup functions for graceful shutdown:

srv.RunServer(handler, host, port, func() error {
    db.Close()
    cache.Close()
    return nil
})

Advanced Configuration

For custom server configurations, use the individual components:

mux := srv.NewMux()
mux.Middleware(srv.LoggingMiddleware)
mux.Middleware(srv.RecoverMiddleware)

server := &http.Server{
    Addr:         ":8080",
    Handler:      mux,
    ReadTimeout:  15 * time.Second,
    WriteTimeout: 15 * time.Second,
    IdleTimeout:  60 * time.Second,
}

log.Fatal(server.ListenAndServe())

Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Add tests for new functionality
  4. Ensure all tests pass
  5. Submit a pull request

License

This package is part of the c3p0-box/utils collection and follows the same license terms.

Documentation

Overview

Package srv provides HTTP server utilities with middleware support, graceful shutdown, enhanced routing with URL reversing, and standardized error handling via the erm package.

This package offers a collection of HTTP middleware components, a server runner that supports graceful shutdown with signal handling, and an enhanced router with URL reversing capabilities. It includes logging and recovery middleware, along with utilities for chaining multiple middleware together. All errors use the erm package for consistent HTTP status codes and internationalized error messages.

Example usage:

mux := srv.NewMux()
mux.Get("users", "/users", func(ctx srv.Context) error {
	return ctx.JSON(200, users)
})
mux.Get("user", "/users/{id}", func(ctx srv.Context) error {
	id := ctx.Param("id")
	editURL, _ := mux.Reverse("user", map[string]string{"id": id})
	return ctx.JSON(200, map[string]interface{}{
		"user": getUser(id),
		"edit_url": editURL,
	})
})

// Chain middleware using the Mux Middleware method
mux.Middleware(srv.LoggingMiddleware)
mux.Middleware(srv.RecoverMiddleware)

// Run server with graceful shutdown
err := srv.RunServer(handler, "localhost", "8080", func() error {
	// cleanup logic here
	return nil
})

Index

Constants

View Source
const (

	// MIMEApplicationJSON JavaScript Object Notation (JSON) https://www.rfc-editor.org/rfc/rfc8259
	MIMEApplicationJSON                  = "application/json"
	MIMEApplicationJavaScript            = "application/javascript"
	MIMEApplicationJavaScriptCharsetUTF8 = MIMEApplicationJavaScript + "; " + charsetUTF8
	MIMEApplicationXML                   = "application/xml"
	MIMEApplicationXMLCharsetUTF8        = MIMEApplicationXML + "; " + charsetUTF8
	MIMETextXML                          = "text/xml"
	MIMETextXMLCharsetUTF8               = MIMETextXML + "; " + charsetUTF8
	MIMEApplicationForm                  = "application/x-www-form-urlencoded"
	MIMEApplicationProtobuf              = "application/protobuf"
	MIMEApplicationMsgpack               = "application/msgpack"
	MIMETextHTML                         = "text/html"
	MIMETextHTMLCharsetUTF8              = MIMETextHTML + "; " + charsetUTF8
	MIMETextPlain                        = "text/plain"
	MIMETextPlainCharsetUTF8             = MIMETextPlain + "; " + charsetUTF8
	MIMEMultipartForm                    = "multipart/form-data"
	MIMEOctetStream                      = "application/octet-stream"
)
View Source
const (
	HeaderAccept         = "Accept"
	HeaderAcceptEncoding = "Accept-Encoding"
	// HeaderAllow is the name of the "Allow" header field used to list the set of methods
	// advertised as supported by the target resource. Returning an Allow header is mandatory
	// for status 405 (method not found) and useful for the OPTIONS method in responses.
	// See RFC 7231: https://datatracker.ietf.org/doc/html/rfc7231#section-7.4.1
	HeaderAllow               = "Allow"
	HeaderAuthorization       = "Authorization"
	HeaderContentDisposition  = "Content-Disposition"
	HeaderContentEncoding     = "Content-Encoding"
	HeaderContentLength       = "Content-Length"
	HeaderContentType         = "Content-Type"
	HeaderCookie              = "Cookie"
	HeaderSetCookie           = "Set-Cookie"
	HeaderIfModifiedSince     = "If-Modified-Since"
	HeaderLastModified        = "Last-Modified"
	HeaderLocation            = "Location"
	HeaderRetryAfter          = "Retry-After"
	HeaderUpgrade             = "Upgrade"
	HeaderVary                = "Vary"
	HeaderWWWAuthenticate     = "WWW-Authenticate"
	HeaderXForwardedFor       = "X-Forwarded-For"
	HeaderXForwardedProto     = "X-Forwarded-Proto"
	HeaderXForwardedProtocol  = "X-Forwarded-Protocol"
	HeaderXForwardedSsl       = "X-Forwarded-Ssl"
	HeaderXUrlScheme          = "X-Url-Scheme"
	HeaderXHTTPMethodOverride = "X-HTTP-Method-Override"
	HeaderXRealIP             = "X-Real-Ip"
	HeaderXRequestID          = "X-Request-Id"
	HeaderXCorrelationID      = "X-Correlation-Id"
	HeaderXRequestedWith      = "X-Requested-With"
	HeaderServer              = "Server"
	HeaderOrigin              = "Origin"
	HeaderCacheControl        = "Cache-Control"
	HeaderConnection          = "Connection"

	// Access control
	HeaderAccessControlRequestMethod    = "Access-Control-Request-Method"
	HeaderAccessControlRequestHeaders   = "Access-Control-Request-Headers"
	HeaderAccessControlAllowOrigin      = "Access-Control-Allow-Origin"
	HeaderAccessControlAllowMethods     = "Access-Control-Allow-Methods"
	HeaderAccessControlAllowHeaders     = "Access-Control-Allow-Headers"
	HeaderAccessControlAllowCredentials = "Access-Control-Allow-Credentials"
	HeaderAccessControlExposeHeaders    = "Access-Control-Expose-Headers"
	HeaderAccessControlMaxAge           = "Access-Control-Max-Age"

	// Security
	HeaderStrictTransportSecurity         = "Strict-Transport-Security"
	HeaderXContentTypeOptions             = "X-Content-Type-Options"
	HeaderXXSSProtection                  = "X-XSS-Protection"
	HeaderXFrameOptions                   = "X-Frame-Options"
	HeaderContentSecurityPolicy           = "Content-Security-Policy"
	HeaderContentSecurityPolicyReportOnly = "Content-Security-Policy-Report-Only"
	HeaderXCSRFToken                      = "X-CSRF-Token"
	HeaderReferrerPolicy                  = "Referrer-Policy"
)

Variables

View Source
var DefaultCORSConfig = CORSConfig{
	AllowOrigins: []string{"*"},
	AllowMethods: []string{
		http.MethodGet,
		http.MethodHead,
		http.MethodPut,
		http.MethodPatch,
		http.MethodPost,
		http.MethodDelete,
		http.MethodOptions,
	},
	AllowHeaders:     []string{},
	AllowCredentials: false,
	ExposeHeaders:    []string{},
	MaxAge:           0,
}

DefaultCORSConfig is the default CORS middleware config.

View Source
var DefaultTrailingSlashConfig = TrailingSlashConfig{
	RedirectCode: 0,
}

DefaultTrailingSlashConfig is the default AddTrailingSlash middleware config.

Functions

func ParseRequest

func ParseRequest(r *http.Request, target interface{}) erm.Error

ParseRequest parses HTTP request payload (JSON or form data) and query parameters into the provided target structure This function automatically detects the Content-Type and uses the appropriate parsing method for the request body, while also parsing URL query parameters regardless of Content-Type

Supported Content-Types: - application/json: Uses json.NewDecoder for efficient streaming - application/x-www-form-urlencoded: Parses form data using reflection and struct tags - multipart/form-data: Parses multipart form data - Empty Content-Type: Attempts to detect based on request body

Features: - Automatic Content-Type detection and routing - Efficient JSON streaming (like json/v2.UnmarshalRead pattern) - Form field mapping using `form` struct tags - Query parameter mapping using `query` struct tags - Type conversion for form and query values (string, int, bool, etc.) - Custom type support for types implementing encoding.TextUnmarshaler interface - Proper error handling with erm.Error types - Resource leak prevention with automatic cleanup - Combined parsing: both request body and query parameters in single call

Example usage:

type UserID [16]byte

func (u *UserID) UnmarshalText(text []byte) error {
	if len(text) != 32 { // hex encoded 16 bytes
		return errors.New("invalid UserID length")
	}
	_, err := hex.Decode(u[:], text)
	return err
}

type CreateUserRequest struct {
	Name     string `json:"name" form:"name"`
	Email    string `json:"email" form:"email"`
	Age      int    `json:"age" form:"age"`
	UserID   UserID `form:"user_id" query:"user_id"`  // Custom type with TextUnmarshaler
	Page     int    `query:"page"`
	Sort     string `query:"sort"`
	FilterBy string `query:"filter_by"`
}

var req CreateUserRequest
if err := ParseRequest(r, &req); err != nil {
	// Handle error
	return err
}
// Use req.Name, req.Email, req.Age (from body)
// Use req.UserID (parsed via UnmarshalText), req.Page, req.Sort, req.FilterBy (from query parameters)

func RunServer

func RunServer(handler http.Handler, host string, port string, cleanup func() error) error

RunServer starts an HTTP server with graceful shutdown capabilities. It listens on the specified host and port, and shuts down gracefully when receiving SIGINT or SIGTERM signals.

Parameters:

  • handler: The HTTP handler to serve requests
  • host: The host to bind to (defaults to "0.0.0.0" if empty)
  • port: The port to listen on (defaults to "8000" if empty)
  • cleanup: A function called during shutdown for resource cleanup

The server performs the following shutdown sequence:

  1. Receives shutdown signal (SIGINT/SIGTERM)
  2. Stops accepting new requests
  3. Waits up to 10 seconds for existing requests to complete
  4. Calls the cleanup function
  5. Returns any errors that occurred

Returns an error if the server fails to start or if shutdown encounters an error.

Example:

err := RunServer(handler, "localhost", "8080", func() error {
	// Close database connections, cleanup resources, etc.
	return db.Close()
})
if err != nil {
	log.Fatal(err)
}

Types

type CORSConfig

type CORSConfig struct {
	// AllowOrigins determines the value of the Access-Control-Allow-Origin
	// response header. This header defines a list of origins that may access the
	// resource. The wildcard characters '*' and '?' are supported and are
	// converted to regex fragments '.*' and '.' accordingly.
	//
	// Security: use extreme caution when handling the origin, and carefully
	// validate any logic. Remember that attackers may register hostile domain names.
	//
	// Optional. Default value []string{"*"}.
	AllowOrigins []string

	// AllowMethods determines the value of the Access-Control-Allow-Methods
	// response header. This header specifies the list of methods allowed when
	// accessing the resource. This is used in response to a preflight request.
	//
	// Optional. Default value []string{"GET", "HEAD", "PUT", "PATCH", "POST", "DELETE", "OPTIONS"}.
	AllowMethods []string

	// AllowHeaders determines the value of the Access-Control-Allow-Headers
	// response header. This header is used in response to a preflight request to
	// indicate which HTTP headers can be used when making the actual request.
	//
	// Optional. Default value []string{}.
	AllowHeaders []string

	// AllowCredentials determines the value of the Access-Control-Allow-Credentials
	// response header. This header indicates whether or not the response to the
	// request can be exposed when the credentials mode is true.
	//
	// Optional. Default value false.
	// Security: avoid using AllowCredentials = true with AllowOrigins = "*".
	AllowCredentials bool

	// ExposeHeaders determines the value of Access-Control-Expose-Headers, which
	// defines a list of headers that clients are allowed to access.
	//
	// Optional. Default value []string{}.
	ExposeHeaders []string

	// MaxAge determines the value of the Access-Control-Max-Age response header.
	// This header indicates how long (in seconds) the results of a preflight
	// request can be cached.
	//
	// Optional. Default value 0 - meaning header is not sent.
	MaxAge int
}

CORSConfig defines the configuration for CORS middleware.

type Context

type Context interface {
	Set(key string, value interface{})
	Get(key string) interface{}
	Request() *http.Request
	Response() http.ResponseWriter
	IsTLS() bool
	IsWebSocket() bool
	Method() string
	Path() string
	Param(key string) string
	Query() url.Values
	QueryParam(key string) string
	FormValue(key string) string
	GetHeader(key string) string
	GetHeaders() http.Header
	Cookie(key string) (*http.Cookie, error)
	Cookies() []*http.Cookie
	SetHeader(key, value string)
	AddHeader(key, value string)
	SetCookie(cookie *http.Cookie)
	JSON(code int, v interface{}) error
	String(code int, text string) error
	Redirect(code int, path string) error
	HTML(code int, html string) error
	HTMLBlob(code int, html []byte) error
	WriteHeader(code int)
}

type CookieStore

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

CookieStore provides an encrypted cookie-based session store implementation. It stores all session data directly in encrypted cookies on the client side, eliminating the need for server-side session storage.

The store uses AES-GCM encryption for authenticated encryption, ensuring both confidentiality and integrity of session data. Session values are serialized using gob encoding before encryption.

This implementation is suitable for stateless applications or when you want to avoid server-side session storage. However, be aware of cookie size limits (typically ~4KB) and ensure your session data fits within these constraints.

func NewCookieStore

func NewCookieStore(name string, key []byte, options *Options) (*CookieStore, error)

NewCookieStore creates a new cookie-based session store with the specified encryption key and options. The encryption key must be 16, 24, or 32 bytes to select AES-128, AES-192, or AES-256 respectively.

The store uses AES-GCM for authenticated encryption, providing both confidentiality and integrity protection for session data.

Example usage:

// Generate a 32-byte key for AES-256
key := make([]byte, 32)
if _, err := rand.Read(key); err != nil {
    log.Fatal(err)
}

// Create cookie store
store, err := srv.NewCookieStore("app-session", key, srv.NewOptions())
if err != nil {
    log.Fatal(err)
}

// Use with session middleware
mux.Middleware(srv.SessionMiddleware(store, "app-session"))

func (*CookieStore) Get

func (c *CookieStore) Get(r *http.Request, name string) (*Session, error)

Get retrieves an existing session from the encrypted cookie. If the cookie doesn't exist, is invalid, or cannot be decrypted, it returns an error.

func (*CookieStore) New

func (c *CookieStore) New(_ *http.Request, name string) (*Session, error)

New creates a new session. Since this is a cookie store, no server-side storage is required. The session will be saved as an encrypted cookie when Save() is called.

func (*CookieStore) Save

func (c *CookieStore) Save(_ *http.Request, w http.ResponseWriter, session *Session) error

Save encrypts the session data and stores it as a cookie. The session values are serialized using gob encoding and then encrypted using AES-GCM before being base64 encoded and stored in the cookie.

type HandlerFunc

type HandlerFunc func(ctx Context) error

HandlerFunc defines a handler function that receives an Context and returns an error. This allows for more elegant error handling compared to traditional http.HandlerFunc. If the handler returns an error, it will be passed to the configured error handler.

Example:

func getUserHandler(ctx Context) error {
    id := ctx.Param("id")
    user, err := getUserByID(id)
    if err != nil {
        return err  // Error will be handled by error handler
    }
    return ctx.JSON(200, user)
}

func LoggingMiddleware

func LoggingMiddleware(next HandlerFunc) HandlerFunc

LoggingMiddleware is a HandlerFunc-based middleware that logs HTTP requests with structured logging using slog. It works directly with the Context interface and maintains the elegant error handling pattern.

The logged information includes:

  • name: "srv.Logging" (logger identifier)
  • method: HTTP method (GET, POST, etc.)
  • path: Request URL path
  • user-agent: Client user agent string
  • remote-addr: Client remote address
  • duration: Request processing time

Note: This middleware cannot capture the exact status code since it works at the HandlerFunc level, but it provides comprehensive logging of request information.

Example:

mux.Middleware(srv.LoggingMiddleware)

func RecoverMiddleware

func RecoverMiddleware(next HandlerFunc) HandlerFunc

RecoverMiddleware is a HandlerFunc-based middleware that recovers from panics during HTTP request processing. It works directly with the Context interface and maintains the elegant error handling pattern.

When a panic occurs, it logs the error using structured logging and converts the panic to an error that can be handled by the error handler.

The panic is logged with:

  • name: "srv.Recover" (logger identifier)
  • error: The recovered panic value
  • path: Request URL path
  • method: HTTP method

Example:

mux.Middleware(srv.RecoverMiddleware)

type HandlerFuncMiddleware

type HandlerFuncMiddleware func(next HandlerFunc) HandlerFunc

HandlerFuncMiddleware represents middleware that works with HandlerFunc. It takes a HandlerFunc and returns a new HandlerFunc, allowing middleware to be chained while maintaining the srv package's error handling pattern. This enables middleware to work directly with the Context interface and maintain the elegant error handling approach.

Example:

func AuthMiddleware(next HandlerFunc) HandlerFunc {
    return func(ctx Context) error {
        token := ctx.GetHeader("Authorization")
        if token == "" {
            return erm.Unauthorized("missing authorization header", nil)
        }
        // Store authenticated user in context
        ctx.Set("user", getUserFromToken(token))
        return next(ctx)  // Continue to next handler
    }
}

func AddTrailingSlashMiddleware

func AddTrailingSlashMiddleware(config TrailingSlashConfig) HandlerFuncMiddleware

AddTrailingSlashMiddleware returns a HandlerFunc-based middleware that adds a trailing slash to request URLs that don't already have one. It works directly with the Context interface and maintains the elegant error handling pattern.

The middleware can either redirect the client to the URL with trailing slash (when RedirectCode is set) or forward the request internally (when RedirectCode is 0).

Security: The middleware includes protection against open redirect vulnerabilities by sanitizing URLs that contain multiple slashes or backslashes.

Example usage:

// Default behavior (internal forward)
mux.Middleware(srv.AddTrailingSlashMiddleware(srv.DefaultTrailingSlashConfig))

// Redirect with 301 status code
config := srv.TrailingSlashConfig{RedirectCode: 301}
mux.Middleware(srv.AddTrailingSlashMiddleware(config))

func CORSMiddleware

func CORSMiddleware(config CORSConfig) HandlerFuncMiddleware

CORSMiddleware returns a HandlerFunc-based CORS middleware that handles Cross-Origin Resource Sharing (CORS) according to the W3C specification. It supports both simple and preflight requests with comprehensive configuration options for security and compatibility.

Features:

  • Origin validation with exact matching or wildcard support
  • Preflight request handling for complex CORS scenarios
  • Credential support with proper security constraints
  • Custom expose headers for client access to response headers
  • Configurable cache control via MaxAge for preflight responses
  • Automatic Vary header management for proper caching behavior
  • Security hardening against CORS misconfigurations

Security Considerations:

  • When AllowCredentials is true and AllowOrigins contains "*", the middleware will echo the specific origin instead of using "*" to prevent security issues
  • Origins are validated with exact string matching to prevent subdomain attacks
  • Always adds appropriate Vary headers for proper cache behavior

Preflight Limitations: For proper CORS support with preflight requests, you need to register OPTIONS handlers for your routes, as HandlerFunc middleware can only process requests that match registered routes. For automatic preflight handling without explicit OPTIONS routes, consider using a different CORS solution.

Example usage:

// Basic configuration (allows all origins)
mux.Middleware(srv.CORSMiddleware(srv.DefaultCORSConfig))

// Production configuration with specific origins and credentials
corsConfig := srv.CORSConfig{
    AllowOrigins:     []string{"https://app.example.com", "https://admin.example.com"},
    AllowMethods:     []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
    AllowHeaders:     []string{"Content-Type", "Authorization", "X-API-Key"},
    AllowCredentials: true,
    ExposeHeaders:    []string{"X-Total-Count", "X-Rate-Limit"},
    MaxAge:           3600, // Cache preflight for 1 hour
}
mux.Middleware(srv.CORSMiddleware(corsConfig))

// Remember to register OPTIONS handlers for preflight support
mux.Options("", "/api/users", func(ctx Context) error { return nil })
mux.Post("", "/api/users", createUser)

func SessionMiddleware

func SessionMiddleware(store Store, sessionName string) HandlerFuncMiddleware

SessionMiddleware returns a HandlerFunc-based session middleware that automatically manages sessions for each request. It loads existing sessions from the store or creates new ones as needed, makes the session available through the Context, and automatically saves the session after the request completes.

The middleware integrates seamlessly with the srv package's Context interface, allowing easy session access via ctx.Get("session") or helper methods.

Example usage:

// Create session store
store := srv.NewInMemoryStore("myapp-session", srv.NewOptions())

// Add session middleware
mux.Middleware(srv.SessionMiddleware(store, "myapp-session"))

// Use in handlers
mux.Get("profile", "/profile", func(ctx srv.Context) error {
	session := ctx.Get("session").(*srv.Session)
	userID := session.Get("userID")
	if userID == nil {
		return ctx.Redirect(302, "/login")
	}
	return ctx.JSON(200, map[string]interface{}{"userID": userID})
})

type HttpContext

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

HttpContext provides a convenient wrapper around http.Request and http.ResponseWriter with additional functionality for storing request-scoped values, handling common HTTP operations, and providing helper methods for request/response processing.

HttpContext is thread-safe for concurrent access to its internal value store.

func NewHttpContext

func NewHttpContext(w http.ResponseWriter, r *http.Request) *HttpContext

NewHttpContext creates a new HttpContext instance wrapping the provided http.ResponseWriter and http.Request. The context includes an empty thread-safe value store for request-scoped data.

func (*HttpContext) AddHeader

func (c *HttpContext) AddHeader(key, value string)

AddHeader adds a response header. If a header with the same key already exists, the value will be appended.

func (*HttpContext) Cookie

func (c *HttpContext) Cookie(key string) (*http.Cookie, error)

Cookie returns the named cookie provided in the request. Returns ErrNoCookie if no cookie with the given name is found.

func (*HttpContext) Cookies

func (c *HttpContext) Cookies() []*http.Cookie

Cookies returns all cookies provided in the request.

func (*HttpContext) FormValue

func (c *HttpContext) FormValue(key string) string

FormValue returns the value of the specified form parameter. It parses the form data if not already parsed.

func (*HttpContext) Get

func (c *HttpContext) Get(key string) interface{}

Get retrieves a value from the context's value store by key. Returns nil if the key doesn't exist. This method is thread-safe and can be called concurrently.

func (*HttpContext) GetHeader

func (c *HttpContext) GetHeader(key string) string

GetHeader returns the value of the specified request header.

func (*HttpContext) GetHeaders

func (c *HttpContext) GetHeaders() http.Header

GetHeaders returns all request headers.

func (*HttpContext) HTML

func (c *HttpContext) HTML(code int, html string) error

HTML writes an HTML response with the specified status code. The Content-Type header is automatically set to "text/html".

func (*HttpContext) HTMLBlob

func (c *HttpContext) HTMLBlob(code int, blob []byte) error

HTMLBlob writes an HTML response with the specified status code. The Content-Type header is automatically set to "text/html".

func (*HttpContext) IsTLS

func (c *HttpContext) IsTLS() bool

IsTLS returns true if the request was made over HTTPS/TLS.

func (*HttpContext) IsWebSocket

func (c *HttpContext) IsWebSocket() bool

IsWebSocket returns true if this is a WebSocket upgrade request. It checks for the "Upgrade: websocket" header.

func (*HttpContext) JSON

func (c *HttpContext) JSON(code int, v interface{}) error

JSON writes a JSON response with the specified status code. The Content-Type header is automatically set to "application/json". Returns an error if JSON encoding fails.

func (*HttpContext) Method

func (c *HttpContext) Method() string

Method returns the HTTP method of the request (GET, POST, etc.).

func (*HttpContext) Param

func (c *HttpContext) Param(key string) string

Param returns the value of the specified path parameter. This uses Go 1.22+ ServeMux path value extraction.

func (*HttpContext) Path

func (c *HttpContext) Path() string

Path returns the URL path of the request.

func (*HttpContext) Query

func (c *HttpContext) Query() url.Values

Query returns all URL query parameters as url.Values.

func (*HttpContext) QueryParam

func (c *HttpContext) QueryParam(key string) string

QueryParam returns the value of the specified query parameter. Returns empty string if the parameter doesn't exist.

func (*HttpContext) Redirect

func (c *HttpContext) Redirect(code int, url string) error

Redirect sends an HTTP redirect response with the specified status code and URL. Common status codes are 301 (permanent), 302 (found), 303 (see other), 307 (temporary), and 308 (permanent redirect).

func (*HttpContext) Request

func (c *HttpContext) Request() *http.Request

Request returns the underlying http.Request object.

func (*HttpContext) Response

func (c *HttpContext) Response() http.ResponseWriter

Response returns the underlying http.ResponseWriter object.

func (*HttpContext) Set

func (c *HttpContext) Set(key string, value interface{})

Set stores a value in the context's value store with the given key. This method is thread-safe and can be called concurrently.

func (*HttpContext) SetCookie

func (c *HttpContext) SetCookie(cookie *http.Cookie)

SetCookie adds a Set-Cookie header to the response.

func (*HttpContext) SetHeader

func (c *HttpContext) SetHeader(key, value string)

SetHeader sets a response header. If a header with the same key already exists, it will be replaced.

func (*HttpContext) SetPath

func (c *HttpContext) SetPath(path string)

SetPath sets the URL path of the request.

func (*HttpContext) String

func (c *HttpContext) String(code int, text string) error

String writes a plain text response with the specified status code. The Content-Type header is automatically set to "text/plain".

func (*HttpContext) WriteHeader

func (c *HttpContext) WriteHeader(code int)

WriteHeader sends an HTTP response header with the provided status code.

type InMemoryStore

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

InMemoryStore provides an in-memory session store implementation. It is thread-safe and suitable for development and single-instance deployments. For production with multiple instances, consider a distributed store.

func NewInMemoryStore

func NewInMemoryStore(name string, options *Options) *InMemoryStore

NewInMemoryStore creates a new in-memory session store with the specified options. It automatically starts a cleanup routine to remove expired sessions.

func (*InMemoryStore) Close

func (s *InMemoryStore) Close()

Close stops the cleanup routine and clears all sessions. This should be called when the store is no longer needed.

func (*InMemoryStore) Get

func (s *InMemoryStore) Get(r *http.Request, name string) (*Session, error)

Get retrieves an existing session or returns nil if not found or expired.

func (*InMemoryStore) New

func (s *InMemoryStore) New(_ *http.Request, name string) (*Session, error)

New creates a new session with a unique ID.

func (*InMemoryStore) Save

func (s *InMemoryStore) Save(_ *http.Request, w http.ResponseWriter, session *Session) error

Save persists the session to the in-memory store and sets the session cookie.

type Mux

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

Mux provides a convenient wrapper around Go's standard http.ServeMux with helper methods for common HTTP operations, RESTful routing, URL reversing, and centralized error handling.

The Mux supports two types of handlers:

  • Traditional http.Handler and http.HandlerFunc via Handle() and HandleFunc()
  • Enhanced HandlerFunc via HTTP method helpers (Get, Post, etc.) with automatic error handling
  • HandlerFunc middleware via the Middleware() method for chaining Context-aware middleware

func NewMux

func NewMux() *Mux

NewMux creates a new Mux instance with an underlying http.ServeMux and a default error handler. The default error handler responds with a 500 Internal Server Error and logs the error. The returned Mux is safe for concurrent use by multiple goroutines.

func (*Mux) Delete

func (m *Mux) Delete(name, pattern string, handler HandlerFunc)

Delete registers a named handler for DELETE requests to the specified pattern. If name is provided, the route can be used for URL generation via the Reverse method. If name is empty string, the route is registered without naming.

func (*Mux) ErrorHandler

func (m *Mux) ErrorHandler(handler func(c Context, err error))

ErrorHandler sets a custom error handler for all HandlerFunc-based routes. The error handler will be called whenever a HandlerFunc returns a non-nil error. If no custom error handler is set, the default handler returns a 500 Internal Server Error.

Example:

mux.ErrorHandler(func(ctx Context, err error) {
    log.Printf("Handler error: %v", err)
    ctx.JSON(500, map[string]string{"error": err.Error()})
})

func (*Mux) Get

func (m *Mux) Get(name, pattern string, handler HandlerFunc)

Get registers a named handler for GET requests to the specified pattern. If name is provided, the route can be used for URL generation via the Reverse method. If name is empty string, the route is registered without naming.

Example:

m.Get("user-list", "/users", handler)                    // Named route
m.Get("", "/health", handler)                            // Unnamed route
url, err := m.Reverse("user-list", nil)                  // Generate: "/users"

func (*Mux) Handle

func (m *Mux) Handle(pattern string, handler http.Handler)

Handle registers a handler for the given pattern.

func (*Mux) HandleFunc

func (m *Mux) HandleFunc(pattern string, handler http.HandlerFunc)

HandleFunc registers a handler function for the given pattern.

func (*Mux) Head

func (m *Mux) Head(name, pattern string, handler HandlerFunc)

Head registers a named handler for HEAD requests to the specified pattern. If name is provided, the route can be used for URL generation via the Reverse method. If name is empty string, the route is registered without naming.

func (*Mux) Middleware

func (m *Mux) Middleware(middleware HandlerFuncMiddleware)

Middleware adds HandlerFunc-based middleware to the Mux. Middleware will be applied to all routes registered after this method is called. Middleware are applied in the order they are added (first added = outermost wrapper).

The middleware function receives the next HandlerFunc in the chain and returns a new HandlerFunc that typically calls the next function with additional logic before, after, or around the call. This allows middleware to work directly with the Context interface and maintain the elegant error handling pattern.

Example:

// Add authentication middleware
mux.Middleware(func(next HandlerFunc) HandlerFunc {
    return func(ctx Context) error {
        token := ctx.GetHeader("Authorization")
        if token == "" {
            return erm.Unauthorized("missing authorization header", nil)
        }
        // Continue to next handler
        return next(ctx)
    }
})

// Add timing middleware
mux.Middleware(func(next HandlerFunc) HandlerFunc {
    return func(ctx Context) error {
        start := time.Now()
        err := next(ctx)
        duration := time.Since(start)
        log.Printf("Request took %v", duration)
        return err
    }
})

func (*Mux) Mux

func (m *Mux) Mux() *http.ServeMux

Mux returns the underlying http.ServeMux for advanced usage or integration with other HTTP libraries.

func (*Mux) Options

func (m *Mux) Options(name, pattern string, handler HandlerFunc)

Options registers a named handler for OPTIONS requests to the specified pattern. If name is provided, the route can be used for URL generation via the Reverse method. If name is empty string, the route is registered without naming.

func (*Mux) Patch

func (m *Mux) Patch(name, pattern string, handler HandlerFunc)

Patch registers a named handler for PATCH requests to the specified pattern. If name is provided, the route can be used for URL generation via the Reverse method. If name is empty string, the route is registered without naming.

func (*Mux) Post

func (m *Mux) Post(name, pattern string, handler HandlerFunc)

Post registers a named handler for POST requests to the specified pattern. If name is provided, the route can be used for URL generation via the Reverse method. If name is empty string, the route is registered without naming.

func (*Mux) Put

func (m *Mux) Put(name, pattern string, handler HandlerFunc)

Put registers a named handler for PUT requests to the specified pattern. If name is provided, the route can be used for URL generation via the Reverse method. If name is empty string, the route is registered without naming.

func (*Mux) Reverse

func (m *Mux) Reverse(name string, params map[string]string) (string, error)

Reverse generates a URL for the named route with the provided parameters. Parameters should be provided as a map where keys match the path parameter names in the route pattern (e.g., "id" for "/users/{id}").

Multiple HTTP methods can share the same route name if they have the same pattern, enabling RESTful route naming (e.g., "users" for both GET /users and POST /users).

Parameters:

  • name: The route name specified when registering the route
  • params: Map of parameter names to values for substitution

Returns the generated URL path and any error encountered. Errors are returned as erm.Error instances: erm.NotFound for unknown routes and erm.RequiredError for missing required parameters.

Example:

m.Get("user-profile", "/users/{id}", handler)
m.Post("users", "/users", handler)

// Generate URLs
profileURL, err := m.Reverse("user-profile", map[string]string{"id": "123"})
// Returns: "/users/123"

usersURL, err := m.Reverse("users", nil)
// Returns: "/users"

// Error handling
_, err := m.Reverse("non-existent", nil)
// Returns: erm.NotFound error with 404 status

_, err := m.Reverse("user-profile", nil)
// Returns: erm.RequiredError for missing {id} parameter

func (*Mux) ServeHTTP

func (m *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP implements http.Handler interface, allowing Mux to be used directly as an HTTP handler.

type Options

type Options struct {
	// Path sets the cookie path. Defaults to "/".
	Path string
	// Domain sets the cookie domain.
	Domain string
	// MaxAge sets the maximum age for the session in seconds.
	// If <= 0, the session cookie will be deleted when the browser closes.
	MaxAge int
	// Secure indicates whether the cookie should only be sent over HTTPS.
	Secure bool
	// HttpOnly indicates whether the cookie should be accessible only through HTTP requests.
	// This prevents access via JavaScript, mitigating XSS attacks.
	HttpOnly bool
	// SameSite controls when cookies are sent with cross-site requests.
	SameSite http.SameSite
}

Options holds configuration for session cookies.

func NewOptions

func NewOptions() *Options

NewOptions returns Options with secure defaults.

type Route

type Route struct {
	Name    string // The route name for URL reversing
	Pattern string // URL pattern (e.g., "/users/{id}")
}

Route represents a named route with its URL pattern. Multiple HTTP methods can share the same route if they have the same pattern. This is used internally for URL reversing functionality.

type Session

type Session struct {
	// The ID of the session, generated by stores. It should not be used for
	// user data.
	ID string
	// Values contains the user-data for the session.
	Values  url.Values
	Options *Options
	IsNew   bool
	// contains filtered or unexported fields
}

Session represents a user session with associated data and configuration. It provides a secure way to maintain state across HTTP requests.

func (*Session) Clear

func (s *Session) Clear()

Clear removes all values from the session.

func (*Session) Delete

func (s *Session) Delete(key string)

Delete removes a key from the session.

func (*Session) Get

func (s *Session) Get(key string) string

Get retrieves a value from the session by key.

func (*Session) Save

func (s *Session) Save(r *http.Request, w http.ResponseWriter) error

Save persists the session to the underlying store.

func (*Session) Set

func (s *Session) Set(key, value string)

Set stores a value in the session with the given key.

type Store

type Store interface {
	// Get should return a cached session.
	Get(r *http.Request, name string) (*Session, error)

	// New should create and return a new session.
	//
	// Note that New should never return a nil session, even in the case of
	// an error if using the Registry infrastructure to cache the session.
	New(r *http.Request, name string) (*Session, error)

	// Save should persist session to the underlying store implementation.
	Save(r *http.Request, w http.ResponseWriter, s *Session) error
}

Store defines the interface for session storage backends. Implementations must be thread-safe.

type TrailingSlashConfig

type TrailingSlashConfig struct {
	// RedirectCode is the HTTP status code used when redirecting the request.
	// If set to 0, the request is forwarded internally without a redirect.
	// If set to a redirect code (e.g., 301, 302), an HTTP redirect is performed.
	//
	// Optional. Default value 0 (forward internally).
	RedirectCode int
}

TrailingSlashConfig defines the configuration for AddTrailingSlash middleware.

Jump to

Keyboard shortcuts

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