httpx

package module
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Apr 2, 2026 License: MIT Imports: 13 Imported by: 0

README

httpx

Extended HTTP utilities for Go, extending net/http with middleware support, error handling, and composable mux types.

Installation

go get github.com/eriicafes/httpx

Features

  • Error-returning handlers - Write handlers that return errors
  • Graceful shutdown - Built-in graceful server shutdown
  • Middleware support - Apply middlewares to routes
  • Route prefixing - Group routes under a common prefix
  • Trailing slash normalization - Automatically handle routes with/without trailing slashes
  • Custom error handling - Define how errors from handlers are processed
  • Composable - Chain multiple mux types together
  • OpenAPI - Generate OpenAPI specs from route definitions with full schema reflection

Subpackages

  • httperrors - Structured HTTP error handling with status codes and details
  • contextkey - Type-safe context key management with generics
  • session - Session-based authentication, cookies, and flash messages
  • openapi - OpenAPI spec generation with type-safe route documentation

Quick Start

Basic Usage
import "github.com/eriicafes/httpx"

mux := httpx.New()

// Regular handler
mux.HandleFunc("GET /hello", func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "Hello, World!")
})

// Error-returning handler with different error types
mux.Route("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) error {
    id := r.PathValue("id")
    user, err := getUser(id)
    if err != nil {
        // Internal error - logs underlying error, returns user-friendly message
        // Logs: "[ERROR] GET /users/123: sql: no rows in result set"
        // Returns: {"error": "Failed to retrieve user"} with status 500
        return httpx.InternalError(err, "Failed to retrieve user")
    }

    // HTTP error - custom status code and message
    // Returns: {"error": "User email not verified"} with status 403
    if !user.Verified {
        return httperrors.New("User email not verified", http.StatusForbidden)
    }

    // plain error - returns 400 with error message
    // Returns: {"error": "user has been deleted"} with status 400
    if user.Deleted {
        return fmt.Errorf("user has been deleted")
    }

    return httpx.Send(w, user)
})

httpx.ListenAndServe(":8080", mux, nil)
Graceful Shutdown

Start your server with automatic graceful shutdown:

import "github.com/eriicafes/httpx"

mux := httpx.New()
mux.HandleFunc("GET /", handler)

// Simple usage with defaults (30s shutdown timeout)
httpx.ListenAndServe(":8080", mux, nil)

// Custom shutdown timeout
httpx.ListenAndServe(":8080", mux, &httpx.ServerConfig{
    ShutdownTimeout: 60 * time.Second,
})

// Custom server configuration
httpx.ListenAndServe(":8080", mux, &httpx.ServerConfig{
    Server: &http.Server{
        ReadTimeout:  10 * time.Second,
        WriteTimeout: 15 * time.Second,
        IdleTimeout:  120 * time.Second,
    },
    ShutdownTimeout: 45 * time.Second,
})

The server gracefully shuts down when receiving SIGINT/SIGTERM signals, completing in-flight requests before stopping.

Middleware
import "github.com/eriicafes/httpx"

func loggingMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		log.Printf("[logging] before %s %s", r.Method, r.URL.Path)
		next.ServeHTTP(w, r)
		log.Printf("[logging] after %s %s", r.Method, r.URL.Path)
	})
}

func authMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		log.Println("[auth] before")
		next.ServeHTTP(w, r)
		log.Println("[auth] after")
	})
}

// Apply middleware to all routes
mux := httpx.Use(
	http.NewServeMux(),
	loggingMiddleware,
	authMiddleware,
)

mux.HandleFunc("GET /users", handler)

Request life cycle:

Request enters
    |
    v
+-----------------------+
| loggingMiddleware     |
|  - before             |
+-----------------------+
            |
            v
        +-----------------------+
        | authMiddleware        |
        |  - before             |
        +-----------------------+
                    |
                    v
              +------------------+
              |   /users handler |
              +------------------+
                    |
                    v
        +-----------------------+
        | authMiddleware        |
        |  - after              |
        +-----------------------+
            |
            v
+-----------------------+
| loggingMiddleware     |
|  - after              |
+-----------------------+
    |
    v
Response exits
Route Prefixing
import "github.com/eriicafes/httpx"

mux := http.NewServeMux()
api := httpx.Prefix(mux, "/api/v1")

// GET /api/v1/users
api.HandleFunc("GET /users", listUsers)
// GET /api/v1/users/{id}
api.HandleFunc("GET /users/{id}", getUser)

http.ListenAndServe(":8080", mux)
Group

Group registers a group of routes by calling sub with a Prefixed mux.

mux := httpx.New()

httpx.Group(mux, "/api/v1", func(mux httpx.Mux) {
    mux = httpx.Use(mux, authMiddleware)
    mux.Route("GET /users", listUsers)   // GET /api/v1/users
    mux.Route("POST /users", createUser) // POST /api/v1/users
})

http.ListenAndServe(":8080", mux)
Trailing Slash Normalization

By default, Go's mux treats trailing slash patterns differently:

mux := http.NewServeMux()

mux.HandleFunc("/api", h)     // base: exact match only
// GET /api        → 200
// GET /api/       → 404
// GET /api/nested → 404

mux.HandleFunc("/api/", h)    // subtree: matches /api/ and all paths below
// GET /api        → 301 redirect to /api/
// GET /api/       → 200
// GET /api/nested → 200

mux.HandleFunc("/api/{$}", h) // exact trailing slash: matches /api/ only
// GET /api        → 301 redirect to /api/
// GET /api/       → 200
// GET /api/nested → 404

To support both /api and /api/, you would have to register /api/{$} manually and still accept that /api redirects to /api/ instead of being served directly. NormalizeTrailingSlash solves this by registering both /api and /api/{$}:

mux := httpx.NormalizeTrailingSlash(http.NewServeMux())

mux.HandleFunc("/api", h) // registers both /api and /api/{$}
// GET /api        → 200
// GET /api/       → 200
// GET /api/nested → 404

http.ListenAndServe(":8080", mux)

Patterns already ending with / or /{$} are unchanged.

Custom Error Handling

By default, Route() and httpx.HandlerFunc use the default error handler which sends JSON responses with the following behavior:

  • Regular errors: Status 400 (Bad Request) with {"error": "error message"}
  • Internal errors (wrapped with InternalError): Status 500 (Internal Server Error) with user-friendly message, logs underlying error server-side
  • HTTP errors (httperrors.HTTPError): Custom status codes and details

You can customize error handling using Fallback():

import (
    "github.com/eriicafes/httpx"
    "github.com/eriicafes/httpx/httperrors"
)

errorHandler := func(w http.ResponseWriter, r *http.Request, err error) {
    // Handle structured HTTP errors
    if httpErr, ok := httperrors.Unwrap(err); ok {
        message, statusCode, _ := httpErr.HTTPError()
        http.Error(w, message, statusCode)
        return
    }
    // Fallback for other errors
    log.Println("Error:", r.Method, r.URL.Path, err.Error())
    http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}

mux := httpx.Fallback(http.NewServeMux(), errorHandler)

mux.Route("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) error {
    user, err := getUser(r.PathValue("id"))
    if err != nil {
        return err
    }
    return httpx.Send(w, user)
})
Composing Mux
mux := httpx.New()
mux = httpx.NormalizeTrailingSlash(mux)
mux = httpx.Prefix(mux, "/api/v1")
mux = httpx.Use(mux, loggingMiddleware, authMiddleware)
mux = httpx.Fallback(mux, errorHandler)

// Handler with trailing slash normalization, /api/v1 prefix,
// middleware, and custom error handling
mux.Route("GET /users", func(w http.ResponseWriter, r *http.Request) error {
    // handle request
    return nil
})

Advanced

Retrieving Error Handlers

Get a mux's error handler or the default if none is configured:

parentHandler := httpx.MuxErrorHandler(parentMux)

childErrorHandler := func(w http.ResponseWriter, r *http.Request, err error) {
    if errors.Is(err, ErrUnauthorized) {
        http.Error(w, "Unauthorized", http.StatusUnauthorized)
        return
    }
    // Delegate to parent
    parentHandler.HandleError(w, r, err)
}

childMux := httpx.Fallback(parentMux, childErrorHandler)
Converting to http.Handler

When you need to pass an http.Handler somewhere (like http.Handle, middleware, or custom mux implementations), convert error-returning handlers using one of these approaches:

1. Using HandlerFunc (default error handler):

// Uses the default error handler
handler := httpx.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
    user, err := getUser(r.PathValue("id"))
    if err != nil {
        return httpx.InternalError(err, "Failed to retrieve user")
    }
    return httpx.Send(w, user)
})

http.Handle("GET /users/{id}", handler)

2. Using Handler (reuses mux's error handler):

Useful when you have an existing mux with custom error handling and want to reuse it:

// Create mux with custom error handling
mux := httpx.Fallback(http.NewServeMux(), customErrorHandler)

handler := func(w http.ResponseWriter, r *http.Request) error {
    user, err := getUser(r.PathValue("id"))
    if err != nil {
        return err
    }
    return httpx.Send(w, user)
}

// Reuses mux's custom error handler
http.Handle("GET /users/{id}", httpx.Handler(mux, handler))
Converting Existing Mux

Wrap an existing mux to add httpx features:

// Existing mux
existingMux := http.NewServeMux()
existingMux.HandleFunc("GET /", handlerFunc)

// Add httpx features
mux := httpx.Use(existingMux)
mux = httpx.Fallback(mux, errorHandler)

// Use error-returning handlers
mux.Route("GET /api/data", func(w http.ResponseWriter, r *http.Request) error {
    return nil
})

// Original routes still work
http.ListenAndServe(":8080", mux)
Implementing Custom Mux

Create custom mux types by implementing the Mux interface:

type LoggingMux struct {
    mux httpx.ServeMux
}

// Expose underlying mux for error handler delegation
func (m *LoggingMux) Mux() httpx.ServeMux {
    return m.mux
}

func (m *LoggingMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    m.mux.ServeHTTP(w, r)
}

func (m *LoggingMux) Handle(pattern string, handler http.Handler) {
    log.Printf("Registering route: %s", pattern)
    m.mux.Handle(pattern, handler)
}

func (m *LoggingMux) HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request)) {
    log.Printf("Registering route: %s", pattern)
    m.mux.HandleFunc(pattern, handler)
}

func (m *LoggingMux) Route(pattern string, handler func(http.ResponseWriter, *http.Request) error) {
    m.Handle(pattern, httpx.Handler(m, handler))
}

// Usage
func NewLoggingMux(mux httpx.ServeMux) httpx.Mux {
    return &LoggingMux{mux: mux}
}

mux := NewLoggingMux(http.NewServeMux())

Key points:

  • Implement ServeHTTP, Handle, HandleFunc, and Route methods
  • Use Mux() to expose the underlying mux for error handler delegation
  • Use httpx.Handler(mux, handler) in Route() to wrap error-returning handlers

OpenAPI

The openapi subpackage generates an OpenAPI spec from your route definitions. Schemas are reflected from Go types at startup, with full support for component registration via $ref. The default OpenAPI version is 3.1.0, configurable via doc.OpenAPIVersion.

Installation
go get github.com/eriicafes/httpx/openapi
Setup

Wrap your mux with openapi.WithRouter, then retrieve the router with openapi.UseRouter to register operations. Serve the spec as JSON or as an interactive Scalar reference UI.

import (
    "github.com/eriicafes/httpx"
    "github.com/eriicafes/httpx/openapi"
    "github.com/eriicafes/httpx/openapi/doc"
    "github.com/eriicafes/httpx/openapi/op"
    "github.com/eriicafes/httpx/openapi/resp"
)

mux := httpx.New()
mux = openapi.WithRouter(mux,
    doc.Info("My API", "1.0.0"),
    doc.Description("API description"),
    doc.Server("https://api.example.com", "Production"),
    doc.Server("http://localhost:8080", "Local"),
    doc.Tag("users", "User management"),
    doc.Security("bearerAuth"),
)

router := openapi.UseRouter(mux)

router.Route("GET /users",
    op.Options(
        op.Summary("List users"),
        op.Tags("users"),
        op.QueryParam[string]("search"),
        op.QueryParam[int]("page"),
        op.Response[[]User](200),
        op.Response[ErrorResponse](401),
    ),
    httpx.HandlerFunc(listUsers),
)

router.Route("POST /users",
    op.Options(
        op.Summary("Create user"),
        op.Tags("users"),
        op.RequestBody[CreateUserRequest](),
        op.Response[User](201),
        op.Response[ErrorResponse](400),
    ),
    httpx.HandlerFunc(createUser),
)

router.Route("DELETE /users/{id}",
    op.Options(
        op.Summary("Delete user"),
        op.Tags("users"),
        op.PathParam[int]("id"),
        op.Security("bearerAuth", "users:write"),
        op.Response[op.NoContent](204),
        op.Response[ErrorResponse](404),
    ),
    httpx.HandlerFunc(deleteUser),
)

// Serve the spec and reference UI
mux.Handle("GET /docs", router.OpenAPIHandler())
mux.Handle("GET /docs/reference", router.ReferenceHandler(nil))
Schema Customisation

Types implement schema.Schema to annotate their generated schema:

type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

func (u User) Schema() schema.Option {
    return schema.Options(
        schema.Ref("User"),  // registers as $ref: "#/components/schemas/User"
        schema.Field("name", schema.MinLength(3), schema.MaxLength(50)),
        schema.Field("email", schema.Email(), schema.Example("user@example.com")),
    )
}
Sub-packages
Package Purpose
openapi/doc Document-level options (Info, Server, Tag, Security, …)
openapi/op Operation options (Summary, Tags, RequestBody, Response, PathParam, QueryParam, …)
openapi/resp Response options (Description, Header, Link, ContentType, …)
openapi/body Request body options (Description, Required, ContentType, …)
openapi/param Parameter options (Description, Required, Style, Explode, Example, …)
openapi/header Response header options
openapi/schema JSON Schema options (Ref, Title, Description, Format, Minimum, Maximum, Enum, AllOf, OneOf, …)

License

MIT

Documentation

Overview

Package httpx provides extended HTTP utilities for Go, extending net/http with error-returning handlers, middleware support, graceful shutdown, and composable mux wrappers.

Error-Returning Handlers

The core feature is handlers that return errors instead of manually writing error responses:

mux := httpx.New()
mux.Route("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) error {
    user, err := getUser(r.PathValue("id"))
    if err != nil {
        return httpx.InternalError(err, "Failed to retrieve user")
    }
    return httpx.Send(w, user)
})

Errors are automatically converted to JSON responses with appropriate status codes:

  • Regular errors: 400 Bad Request
  • Internal errors (httpx.InternalError): 500 Internal Server Error (logged server-side)
  • HTTP errors (httperrors.HTTPError): Custom status codes and details

Graceful Shutdown

The ListenAndServe function provides automatic graceful shutdown on SIGINT/SIGTERM:

httpx.ListenAndServe(":8080", mux, &httpx.ServerConfig{
    ShutdownTimeout: 30 * time.Second,
})

Middleware

Apply middleware to routes using the Use wrapper:

mux := httpx.Use(
    http.NewServeMux(),
    loggingMiddleware,
    authMiddleware,
)

Composable Mux Wrappers

Chain multiple mux wrappers for layered functionality:

mux := httpx.New()
mux = httpx.NormalizeTrailingSlash(mux)
mux = httpx.Prefix(mux, "/api/v1")
mux = httpx.Use(mux, corsMiddleware)
mux = httpx.Fallback(mux, customErrorHandler)

Available mux wrappers:

  • Prefix: Add common prefix to routes
  • Use: Apply middleware
  • Fallback: Custom error handling
  • NormalizeTrailingSlash: Handle routes with/without trailing slashes

Request and Response Helpers

Simplified JSON encoding and decoding:

// Read request body
user, err := httpx.Read[User](r)

// Send response
httpx.Send(w, user)
httpx.SendStatus(w, http.StatusCreated, user)

Subpackages

The httpx module includes specialized subpackages:

  • httperrors: Structured HTTP error handling with status codes
  • contextkey: Type-safe context key management
  • session: Session-based authentication with cookies and flash messages

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Group added in v0.4.0

func Group(mux ServeMux, prefix string, sub func(Mux))

Group registers a group of routes by calling sub with a Prefixed mux.

Prefix concatenates the prefix with each registered pattern and registers the resulting pattern on the underlying mux. Any HTTP method specified in the pattern (for example "GET /users") is preserved in the resulting pattern. No other parsing, normalization, or rewriting is performed.

func Handler

func Handler(mux ServeMux, handler func(http.ResponseWriter, *http.Request) error) http.Handler

Handler converts a handler function that returns an error into an http.Handler. Handler(mux, f) is a http.Handler that calls f. If returned error is non-nil, it handles the error using the error handler extracted from mux. If mux is nil, the default error handler is used.

The default error handler replies to the request with a JSON response of the error message and status code based on error type:

func InternalError

func InternalError(err error, message string) error

InternalError wraps an error with a user-friendly message for client responses. If message is empty, it defaults to "Something went wrong".

When used with the default error handler, it logs the underlying error (with request method and path) while returning only the user-friendly message to the client. This prevents internal implementation details from leaking while maintaining debug visibility.

user, err := getUser(id)
if err != nil {
    // Logs: "[ERROR] GET /api/users/123: Failed to retrieve user: sql: no rows in result set"
    // Returns to client: {"error": "Failed to retrieve user"}
    return httpx.InternalError(err, "Failed to retrieve user")
}

func ListenAndServe

func ListenAndServe(addr string, handler http.Handler, config *ServerConfig) error

ListenAndServe starts an HTTP server with graceful shutdown. It listens on the TCP network address addr and calls Serve with handler to handle requests.

If handler is nil, http.DefaultServeMux is used. If config is nil, default configuration is used (default server, 30s shutdown timeout).

The server will shutdown gracefully when it receives SIGINT or SIGTERM signals. During shutdown, the server will stop accepting new connections and wait for existing connections to finish, up to config.ShutdownTimeout.

ListenAndServe returns when the server has shut down completely. If the server fails to start, it returns the error immediately. If shutdown completes successfully, it returns nil. If shutdown exceeds the timeout, it returns context.DeadlineExceeded.

func MuxPrefix added in v0.4.0

func MuxPrefix(mux ServeMux) string

MuxPrefix returns the cumulative path prefix accumulated by any Prefix mux types in the chain. Returns an empty string if no prefix is found.

func Read

func Read[T any](r *http.Request) (data T, err error)

Read decodes the JSON request body into T. Returns an error if the body is malformed JSON, or has wrong Content-Type. If Content-Type header is present, it must be "application/json" (or with charset).

user, err := httpx.Read[User](r)
if err != nil {
    return httperrors.New(err.Error(), http.StatusBadRequest)
}

func Send

func Send(w http.ResponseWriter, v any) error

Send encodes v as JSON and writes it to the response. It returns an error if encoding fails.

return httpx.Send(w, user)

func SendStatus

func SendStatus(w http.ResponseWriter, statusCode int, v any) error

SendStatus encodes v as JSON and writes it to the response with the given status code. It returns an error if encoding fails.

return httpx.SendStatus(w, http.StatusCreated, user)

Types

type ErrorHandler

type ErrorHandler interface {
	HandleError(http.ResponseWriter, *http.Request, error)
}

func MuxErrorHandler

func MuxErrorHandler(mux ServeMux) ErrorHandler

MuxErrorHandler returns the error handler for mux. If mux is nil or does not implement the ErrorHandler interface, the default error handler is returned.

The default error handler replies to the request with a JSON response of the error message and status code based on error type:

type HandlerFunc

type HandlerFunc func(http.ResponseWriter, *http.Request) error

HandlerFunc allows the use of handler functions that return an error as HTTP handlers. HandlerFunc(f) is a http.Handler that calls f. If returned error is non-nil, it handles the error using the default error handler.

The default error handler replies to the request with a JSON response of the error message and status code based on error type:

func (HandlerFunc) ServeHTTP

func (f HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request)

type JSON

type JSON map[string]any

type Middleware

type Middleware func(handler http.Handler) http.Handler

Middleware is a function that accepts a handler and returns another handler. Middlewares can decide to call or not to call the handler and they can run before or after the handler.

type Mux

type Mux interface {
	ServeMux
	Mux() ServeMux
	Route(pattern string, handler func(http.ResponseWriter, *http.Request) error)
}

Mux is a specialized mux that can register routes with handlers that return an error.

func Fallback

func Fallback(mux ServeMux, errorHandler func(http.ResponseWriter, *http.Request, error)) Mux

Fallback returns a Mux which executes errorHandler when the handler returns a non-nil error.

func New

func New() Mux

func NormalizeTrailingSlash

func NormalizeTrailingSlash(mux ServeMux) Mux

NormalizeTrailingSlash returns a Mux that registers an exact trailing slash variant for each base pattern, so both "/path" and "/path/" match the same handler. Patterns already ending with "/" or "/{$}" are unchanged.

By default, Go's mux treats these pattern types as follows:

mux := http.NewServeMux()
mux.HandleFunc("/api", h)     // base: exact match only
  → GET /api        → 200
  → GET /api/       → 404
  → GET /api/nested → 404

mux.HandleFunc("/api/", h)    // subtree: matches /api/ and all paths below
  → GET /api        → 301 redirect to /api/
  → GET /api/       → 200
  → GET /api/nested → 200

mux.HandleFunc("/api/{$}", h) // exact trailing slash: matches /api/ only
  → GET /api        → 301 redirect to /api/
  → GET /api/       → 200
  → GET /api/nested → 404

To support both "/api" and "/api/", you would have to register "/api/{$}" manually and still accept that "/api" redirects to "/api/" instead of being served directly. NormalizeTrailingSlash solves this by registering both "/api" and "/api/{$}":

mux := httpx.NormalizeTrailingSlash(mux)
mux.HandleFunc("/api", h) // registers both /api and /api/{$}
  → GET /api        → 200
  → GET /api/       → 200
  → GET /api/nested → 404

func Prefix

func Prefix(mux ServeMux, prefix string) Mux

Prefix returns a Mux that registers routes under the given prefix.

Prefix concatenates the prefix with each registered pattern and registers the resulting pattern on the underlying mux. Any HTTP method specified in the pattern (for example "GET /users") is preserved in the resulting pattern. No other parsing, normalization, or rewriting is performed.

func Use

func Use(mux ServeMux, middlewares ...Middleware) Mux

Use returns a Mux which applies middlewares to the handler. Use with an empty middlewares list can be used to convert a *http.ServeMux to a Mux.

type ServeMux

type ServeMux interface {
	http.Handler
	Handle(pattern string, handler http.Handler)
	HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request))
}

ServeMux is the generic interface for route registering mux.

type ServerConfig

type ServerConfig struct {
	// Server is the HTTP server configuration.
	// If nil, a default http.Server will be used.
	Server *http.Server

	// ShutdownTimeout is the maximum duration to wait for graceful shutdown.
	// Default: 30 seconds.
	ShutdownTimeout time.Duration
}

ServerConfig configures the HTTP server and graceful shutdown.

Directories

Path Synopsis
Package contextkey provides type-safe context key management using Go generics.
Package contextkey provides type-safe context key management using Go generics.
Package httperrors provides a structured way to handle HTTP errors with status codes and details.
Package httperrors provides a structured way to handle HTTP errors with status codes and details.
Package openapi builds an OpenAPI document alongside httpx route registration.
Package openapi builds an OpenAPI document alongside httpx route registration.
doc
op
tag
Package session implements session based authentication type-safe cookies, and flash messages.
Package session implements session based authentication type-safe cookies, and flash messages.

Jump to

Keyboard shortcuts

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