httputil

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Mar 29, 2025 License: MIT Imports: 18 Imported by: 0

README

httputil

Package httputil provides utility helpers for working with net/http, adding sensible defaults, bootstrapping, and eliminating boilerplate code commonly required when building web services. This package aims to streamline the development of HTTP-based applications by offering a cohesive set of tools for HTTP server configuration, request handling, error management, and more.

Test Coverage Go Report Card License

Features

HTTP Server with Sensible Defaults
  • Configurable HTTP server with secure, production-ready defaults
  • Graceful shutdown handling
  • Customizable timeouts (idle, read, write, shutdown)
  • Request body size limits to prevent abuse
Handler Framework
  • Easy conversion between different handler types
  • Support for standard http.Handler interfaces
  • JSON request/response handling with automatic marshaling/unmarshaling
  • Request interception and middleware support
Error Handling
  • RFC 7807 compliant problem details for HTTP APIs
  • Standardized error formatting
  • Predefined error constructors for common HTTP status codes
Request Parameter Processing
  • Safe and convenient parameter extraction from different sources (URL, query, headers, body)
  • Validation support
Testing Utilities
  • JSON comparison tools for testing HTTP responses
  • Helper functions to reduce test boilerplate

Installation

go get github.com/nickbryan/httputil

Usage

Basic JSON Handler
package main

import (
    "context"
    "net/http"

    "github.com/nickbryan/slogutil"

    "github.com/nickbryan/httputil"
)

func main() {
    logger := slogutil.NewJSONLogger()  
    server := httputil.NewServer(logger)
	
    server.Register(
        httputil.Endpoint{
            Method: http.MethodGet, 
            Path:   "/greetings", 
            Handler: httputil.NewJSONHandler(
                func(_ httputil.RequestEmpty) (*httputil.Response, error) {
                    return httputil.OK([]string{"Hello, World!", "Hola Mundo!"})
                }, 
            ),
        }, 
    )

    server.Serve(context.Background())

    // curl localhost:8080/greetings
    // ["Hello, World!","Hola Mundo!"]
}
JSON Handler Request/Response
package main

import (
    "context"
    "net/http"

    "github.com/nickbryan/slogutil"

    "github.com/nickbryan/httputil"
)

func main() {
    logger := slogutil.NewJSONLogger()
    server := httputil.NewServer(logger)

    server.Register(newGreetingsEndpoint())

    server.Serve(context.Background())

    // curl -iS -X POST -H "Content-Type: application/json" -d '{"name": "Nick"}' localhost:8080/greetings                                                                               7 ↵
    // HTTP/1.1 201 Created
    // Content-Type: application/json
    // Date: Sat, 29 Mar 2025 17:12:40 GMT
    // Content-Length: 26
    //
    // {"message":"Hello Nick!"}
}

func newGreetingsEndpoint() httputil.Endpoint {
    type (
        request struct {
            Name string `json:"name" validate:"required"`
        }
        response struct {
            Message string `json:"message"`
        }
    )

    return httputil.Endpoint{
        Method: http.MethodPost,
        Path:   "/greetings",
        Handler: httputil.NewJSONHandler(func(r httputil.RequestData[request]) (*httputil.Response, error) {
            return httputil.Created(response{Message: "Hello " + r.Data.Name + "!"})
        }),
    }
}
JSON Handler With Params
package main

import (
    "context"
    "net/http"

    "github.com/nickbryan/slogutil"

    "github.com/nickbryan/httputil"
)

func main() {
    logger := slogutil.NewJSONLogger()
    server := httputil.NewServer(logger)

    server.Register(newGreetingsEndpoint())

    server.Serve(context.Background())

    // curl localhost:8080/greetings/Nick
    // ["Hello, Nick!","Hola Nick!"]
}

func newGreetingsEndpoint() httputil.Endpoint {
    type params struct {
        Name string `path:"name" validate:"required"`
    }

    return httputil.Endpoint{
        Method: http.MethodGet,
        Path:   "/greetings/{name}",
        Handler: httputil.NewJSONHandler(func(r httputil.RequestParams[params]) (*httputil.Response, error) {
            return httputil.OK([]string{"Hello, " + r.Params.Name + "!", "Hola " + r.Params.Name + "!"})
        }),
    }
}
Basic net/http Handler
package main

import (
    "context"
    "net/http"

    "github.com/nickbryan/slogutil"

    "github.com/nickbryan/httputil"
)

func main() {
    logger := slogutil.NewJSONLogger()
    server := httputil.NewServer(logger)

    server.Register(
        httputil.Endpoint{
            Method: http.MethodGet,
            Path:   "/greetings",
            Handler: httputil.NewNetHTTPHandlerFunc(
                func(w http.ResponseWriter, _ *http.Request) {
                    _, _ = w.Write([]byte(`["Hello, World!","Hola Mundo!"]`))
                },
            ),
        },
    )

    server.Serve(context.Background())
	
    // curl localhost:8080/greetings
    // ["Hello, World!","Hola Mundo!"]
}

Server Configuration Options

The httputil.Server can be configured with the following options:

Option Default Description
WithAddress :8080 Sets the address the server will listen on
WithIdleTimeout 30s Controls how long connections are kept open when idle
WithMaxBodySize 5MB Maximum allowed request body size
WithReadHeaderTimeout 5s Maximum time to read request headers
WithReadTimeout 60s Maximum time to read the entire request
WithShutdownTimeout 30s Time to wait for connections to close during shutdown
WithWriteTimeout 30s Maximum time to write a response

Design Choices

RFC 7807 Problem Details

Error responses follow the RFC 7807 standard for Problem Details for HTTP APIs, providing consistent, readable error information.

Middleware Architecture

Middleware can be applied at both the server and endpoint level, providing a flexible way to implement cross-cutting concerns like logging, authentication, and metrics.

Handler Interfaces

The package provides a consistent interface for handlers while supporting multiple styles (standard http.Handler, functional handlers, and JSON-specific handlers).

Documentation

Overview

Package httputil provides utilities for working with HTTP servers and clients.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func BindValidParameters

func BindValidParameters(r *http.Request, output any) error

BindValidParameters extracts parameters from an *http.Request, populates the fields of the output struct and validates the struct. The output parameter must be a pointer to a struct, and the struct fields can be annotated with struct tags to specify the source of the parameters. Supported struct tags and their meanings are:

- `query`: Specifies a query parameter to extract from the URL. - `header`: Specifies an HTTP header to extract from the request. - `path`: Specifies a path parameter to extract. Requires an implementation of r.PathValue(). - `default`: Provides a default value for the parameter if it's not found in the request. - `validate`: Provides rules for the validator.

Example:

 type Params struct {
	  Sort	string  `query:"user_id" validate:"required"`
	  AuthToken string  `header:"Authorization"`
	  Page	  int	 `query:"page" default:"1"`
	  IsActive  bool	`query:"is_active" default:"false"`
	  ID		uuid.UUID `path:"id"`
 }
 var params Params
 if err := BindValidParameters(r, &params); err != nil {
	  return fmt.Errorf("failed to unmarshal params: %v", err)
 }

If a field's type is unsupported, or if a value cannot be converted to the target type, a descriptive error is returned. Supported field types include:

- string - int - bool - float64 - uuid.UUID

Returns problem.BadParameters if: - A value cannot be converted to the target field type. - Validation fails.

Returns an error if: - `output` is not a pointer to a struct. - A default value cannot be converted to the target field type. - A field type in the struct is unsupported.

Types

type Action

type Action[D, P any] func(r Request[D, P]) (*Response, error)

Action defines the interface for an action function that will be called by the handler. It takes a Request that has data of type D and params of type P and returns a Response or an error.

type Endpoint

type Endpoint struct {
	// Method is the HTTP method for this endpoint (e.g., "GET", "POST", "PUT",
	// "DELETE").
	Method string
	// Path is the URL path for this endpoint (e.g., "/users", "/products/{id}").
	Path string
	// Handler is the [Handler] that will handle requests for this endpoint.
	Handler Handler
	// contains filtered or unexported fields
}

Endpoint represents an HTTP endpoint with a method, path, and handler.

func NewEndpointWithRequestInterceptor

func NewEndpointWithRequestInterceptor(e Endpoint, ri RequestInterceptor) Endpoint

NewEndpointWithRequestInterceptor associates the given RequestInterceptor with the specified Endpoint. It returns a new Endpoint with the RequestInterceptor applied. The original Endpoint remains unmodified.

type EndpointGroup

type EndpointGroup []Endpoint

EndpointGroup represents a group of Endpoint definitions allowing access to helper functions to define the group.

func (EndpointGroup) WithMiddleware

func (eg EndpointGroup) WithMiddleware(middleware MiddlewareFunc) EndpointGroup

WithMiddleware applies the given middleware to all provided endpoints. It returns a new slice of EndpointGroup with the middleware applied to their handlers. The original endpoints are not modified.

func (EndpointGroup) WithPrefix

func (eg EndpointGroup) WithPrefix(prefix string) EndpointGroup

WithPrefix prefixes the given path to all provided endpoints. It returns a new slice of EndpointGroup with the prefixed paths. The original endpoints are not modified.

func (EndpointGroup) WithRequestInterceptor

func (eg EndpointGroup) WithRequestInterceptor(ri RequestInterceptor) EndpointGroup

WithRequestInterceptor adds the RequestInterceptor as a RequestInterceptorStack with the currently set RequestInterceptor as the second RequestInterceptor in the stack. It returns a new slice of EndpointGroup with the RequestInterceptor set. The original endpoints are not modified.

type Handler

type Handler interface {
	http.Handler
	// contains filtered or unexported methods
}

Handler represents an interface that combines HTTP handling and additional interceptor and logging functionality ensuring that dependencies can be passed through to the handler.

func NewJSONHandler

func NewJSONHandler[D, P any](action Action[D, P]) Handler

NewJSONHandler creates a new Handler that wraps the provided Action to deserialize JSON request bodies and serialize JSON response bodies.

func NewNetHTTPHandler

func NewNetHTTPHandler(h http.Handler) Handler

NewNetHTTPHandler creates a new Handler that wraps the provided http.Handler so that it can be used on an Endpoint definition.

func NewNetHTTPHandlerFunc

func NewNetHTTPHandlerFunc(h http.HandlerFunc) Handler

NewNetHTTPHandlerFunc creates a new Handler that wraps the provided http.HandlerFunc so that it can be used on an Endpoint definition.

type InvalidOutputTypeError

type InvalidOutputTypeError struct {
	ProvidedType any
}

InvalidOutputTypeError is a custom error type for invalid output types.

func (*InvalidOutputTypeError) Error

func (e *InvalidOutputTypeError) Error() string

Error returns the error message describing the provided invalid output type.

type MiddlewareFunc

type MiddlewareFunc func(next http.Handler) http.Handler

MiddlewareFunc defines a function type for HTTP middleware. A MiddlewareFunc takes a http.Handler as input and returns a new http.Handler that wraps the original action with additional logic.

type ParamConversionError

type ParamConversionError struct {
	ParameterType problem.ParameterType
	ParamName     string
	TargetType    string
	Err           error
}

ParamConversionError represents an error that occurs during parameter conversion.

func (*ParamConversionError) Error

func (e *ParamConversionError) Error() string

Error satisfies the error interface for ParamConversionError.

func (*ParamConversionError) Unwrap

func (e *ParamConversionError) Unwrap() error

Unwrap allows ParamConversionError to be used with errors.Is and errors.As.

type Request

type Request[D, P any] struct {
	*http.Request
	// Data holds the request-specific data of generic type D, which is provided
	// when initializing the request. A [Handler] will attempt to decode the Request
	// body into this type.
	Data D
	// Params holds the parameters of generic type P associated with the request,
	// allowing dynamic decoding and validation of Request parameters. See
	// [BindValidParameters] documentation for usage information.
	Params P
	// ResponseWriter is an embedded HTTP response writer used to construct and send
	// the HTTP response. When writing a response via the ResponseWriter directly it
	// is best practice to return a [NothingToHandle] response so that the handler
	// does not try to encode response data or handle errors.
	ResponseWriter http.ResponseWriter
}

Request is a generic HTTP request wrapper that contains request data, parameters, and a response writer.

type RequestData

type RequestData[D any] = Request[D, struct{}]

RequestData represents a Request that expects data but no Params.

type RequestEmpty

type RequestEmpty = Request[struct{}, struct{}]

RequestEmpty represents an empty Request that expects no Prams or data.

type RequestInterceptor

type RequestInterceptor interface {
	InterceptRequest(r *http.Request) (*http.Request, error)
}

RequestInterceptor is an interface for modifying or inspecting HTTP requests before they are processed further. InterceptRequest takes an HTTP request as input, performs operations on it, and returns a potentially modified request or an error. A Handler will format the error accordingly. This is useful for authentication and adding claims to the request context.

type RequestInterceptorFunc

type RequestInterceptorFunc func(r *http.Request) (*http.Request, error)

RequestInterceptorFunc is a function type for modifying or inspecting an HTTP request, potentially returning an altered request. This is useful for authentication and adding claims to the request context.

func (RequestInterceptorFunc) InterceptRequest

func (rif RequestInterceptorFunc) InterceptRequest(r *http.Request) (*http.Request, error)

InterceptRequest applies the RequestInterceptorFunc to modify or inspect the provided HTTP request.

type RequestInterceptorStack

type RequestInterceptorStack []RequestInterceptor

RequestInterceptorStack represents multiple RequestInterceptor instances that will be run in order.

func (RequestInterceptorStack) InterceptRequest

func (ris RequestInterceptorStack) InterceptRequest(r *http.Request) (*http.Request, error)

InterceptRequest will run each RequestInterceptor in order starting from 0. It will continue iteration until a non nil http.Request or error is returned, it will then return the http.Request and error of that call.

type RequestParams

type RequestParams[P any] = Request[struct{}, P]

RequestParams represents a Request that expects Params but no data.

type Response

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

Response represents an HTTP response that holds optional data and the required information to write a response.

func Accepted

func Accepted(data any) (*Response, error)

Accepted creates a new Response object with a status code of http.StatusAccepted (202 Accepted) and the given data.

func Created

func Created(data any) (*Response, error)

Created creates a new Response object with a status code of http.StatusCreated (201 Created) and the given data.

func NewResponse

func NewResponse(code int, data any) *Response

NewResponse creates a new Response object with the given status code and data.

func NoContent

func NoContent() (*Response, error)

NoContent creates a new Response object with a status code of http.StatusNoContent (204 No Content) and an empty struct as data.

func NothingToHandle

func NothingToHandle() (*Response, error)

NothingToHandle returns a nil Response and a nil error, intentionally representing a scenario with no response output so the Handler does not attempt to process a response. This adds clarity when a RequestInterceptor does not block the request or when acting on Request.ResponseWriter directly.

func OK

func OK(data any) (*Response, error)

OK creates a new Response with HTTP status code 200 (OK) containing the provided data.

func Redirect

func Redirect(code int, url string) (*Response, error)

Redirect creates a new Response object with the given status code and an empty struct as data. The redirect url will be set which will indicate to the handler that a redirect should be written.

type Server

type Server struct {
	// Listener is implemented by a *http.Server, the interface allows us to test Serve.
	Listener interface {
		ListenAndServe() error
		Shutdown(ctx context.Context) error
	}
	// contains filtered or unexported fields
}

Server is an HTTP server with graceful shutdown capabilities.

func NewServer

func NewServer(logger *slog.Logger, options ...ServerOption) *Server

NewServer creates a new Server instance with the specified logger and options. The options parameter allows for customization of server settings such as the address and timeouts.

func (*Server) Register

func (s *Server) Register(endpoints ...Endpoint)

Register one or more endpoints with the Server so they are handled by the underlying router.

func (*Server) Serve

func (s *Server) Serve(ctx context.Context)

Serve starts the HTTP server and listens for incoming requests. It gracefully shuts down the server when it receives an SIGINT, SIGTERM, or SIGQUIT signal.

func (*Server) ServeHTTP

func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP delegates the request handling to the underlying router. Exposing ServeHTTP allows endpoints to be tested without a running server.

type ServerOption

type ServerOption func(so *serverOptions)

ServerOption allows default config values to be overridden.

func WithAddress

func WithAddress(address string) ServerOption

WithAddress sets the address that the Server will listen and serve on.

func WithIdleTimeout

func WithIdleTimeout(timeout time.Duration) ServerOption

WithIdleTimeout sets the idle timeout for the server. This determines how long the server will keep an idle connection alive.

func WithMaxBodySize

func WithMaxBodySize(size int64) ServerOption

WithMaxBodySize sets the maximum allowed size for the request body. This limit helps prevent excessive memory usage or abuse from clients sending extremely large payloads.

func WithReadHeaderTimeout

func WithReadHeaderTimeout(timeout time.Duration) ServerOption

WithReadHeaderTimeout sets the timeout for reading the request header. This is the maximum amount of time the server will wait to receive the request headers.

func WithReadTimeout

func WithReadTimeout(timeout time.Duration) ServerOption

WithReadTimeout sets the timeout for reading the request body. This is the maximum amount of time the server will wait for the entire request to be read.

func WithShutdownTimeout

func WithShutdownTimeout(timeout time.Duration) ServerOption

WithShutdownTimeout sets the timeout for gracefully shutting down the server. This is the amount of time the server will wait for existing connections to complete before shutting down.

func WithWriteTimeout

func WithWriteTimeout(timeout time.Duration) ServerOption

WithWriteTimeout sets the timeout for writing the response. This is the maximum amount of time the server will wait to send a response.

type Transformer

type Transformer interface {
	Transform(ctx context.Context) error
}

Transformer allows for operations to be performed on the Request, Response or Params data before it gets finalized. A Transformer will not be called for a standard http.Handler as there is nothing to transform.

type UnsupportedFieldTypeError

type UnsupportedFieldTypeError struct {
	FieldType any
}

UnsupportedFieldTypeError represents an error for unsupported field types.

func (*UnsupportedFieldTypeError) Error

func (e *UnsupportedFieldTypeError) Error() string

Error satisfies the error interface for UnsupportedFieldTypeError.

Directories

Path Synopsis
internal
testutil
Package testutil provides helper functions for writing tests.
Package testutil provides helper functions for writing tests.
Package problem provides utilities for constructing and handling error responses in accordance with RFC 9457 (Problem Details for HTTP APIs).
Package problem provides utilities for constructing and handling error responses in accordance with RFC 9457 (Problem Details for HTTP APIs).
problemtest
Package problemtest provides utilities for creating and testing problems.
Package problemtest provides utilities for creating and testing problems.

Jump to

Keyboard shortcuts

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