httputil

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Sep 13, 2025 License: MIT Imports: 20 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

Table of Contents

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 using the validator package
Testing Utilities
  • JSON comparison tools for testing HTTP responses
  • Helper functions to reduce test boilerplate

Installation

go get github.com/nickbryan/httputil

Quick Start

Here's a minimal example to get you started:

package main

import (
    "context"
    "net/http"
    "log/slog"
    "os"

    "github.com/nickbryan/httputil"
)

func main() {
    // Create a logger
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    
    // Create a server with default options
    server := httputil.NewServer(logger)
    
    // Register an endpoint
    server.Register(
        httputil.Endpoint{
            Method: http.MethodGet,
            Path:   "/hello",
            Handler: httputil.NewHandler(
                func(_ httputil.RequestEmpty) (*httputil.Response, error) {
                    return httputil.OK(map[string]string{"message": "Hello, World!"})
                },
            ),
        },
    )
    
    // Start the server
    server.Serve(context.Background())
}

Server Configuration

httputil.NewServer can be configured with the following options:

Option Default Description
WithServerAddress :8080 Sets the address the server will listen on
WithServerCodec JSON Sets the default codec for request/response encoding
WithServerIdleTimeout 30s Controls how long connections are kept open when idle
WithServerMaxBodySize 5MB Maximum allowed request body size
WithServerReadHeaderTimeout 5s Maximum time to read request headers
WithServerReadTimeout 60s Maximum time to read the entire request
WithServerShutdownTimeout 30s Time to wait for connections to close during shutdown
WithServerWriteTimeout 30s Maximum time to write a response

Example with custom configuration:

server := httputil.NewServer(
    logger,
    httputil.WithServerAddress(":3000"),
    httputil.WithServerMaxBodySize(10 * 1024 * 1024), // 10MB
    httputil.WithServerReadTimeout(30 * time.Second),
)

Request Handling

Basic Handlers

The package provides a flexible handler system that supports different request types:

// Empty request (no body or parameters)
httputil.NewHandler(func(_ httputil.RequestEmpty) (*httputil.Response, error) {
    return httputil.OK(map[string]string{"message": "Hello, World!"})
})

// Request with JSON body
httputil.NewHandler(func(r httputil.RequestData[MyRequestType]) (*httputil.Response, error) {
    // Access request data with r.Data
    return httputil.OK(map[string]string{"message": "Hello, " + r.Data.Name})
})

// Request with path/query parameters
httputil.NewHandler(func(r httputil.RequestParams[MyParamsType]) (*httputil.Response, error) {
    // Access parameters with r.Params
    return httputil.OK(map[string]string{"message": "Hello, " + r.Params.Name})
})
Request Types

The package supports three main request types:

  1. RequestEmpty - For requests without body or parameters
  2. RequestData<T> - For requests with a JSON body of type T
  3. RequestParams<P> - For requests with path/query parameters of type P

You can also combine both data and parameters:

httputil.NewHandler(func(r httputil.Request[MyRequestType, MyParamsType]) (*httputil.Response, error) {
    // Access both r.Data and r.Params
    return httputil.OK(map[string]string{
        "message": "Hello, " + r.Params.Name,
        "details": r.Data.Details,
    })
})
Parameter Binding

Parameters can be bound from different sources using struct tags:

type MyParams struct {
    ID      string `path:"id" validate:"required,uuid"`
    Filter  string `query:"filter"`
    APIKey  string `header:"X-API-Key" validate:"required"`
    Version int    `query:"version" validate:"omitempty,min=1"`
}

Supported parameter sources:

  • path - URL path parameters
  • query - Query string parameters
  • header - HTTP headers
Validation

The package uses go-playground/validator for request validation:

type CreateUserRequest struct {
    Name     string `json:"name" validate:"required,min=2,max=100"`
    Email    string `json:"email" validate:"required,email"`
    Age      int    `json:"age" validate:"required,min=18"`
    Password string `json:"password" validate:"required,min=8"`
}

Validation errors are automatically converted to RFC 7807 problem details responses.

Handler Options

When creating handlers with httputil.NewHandler(), you can customize their behavior using the following options:

Option Default Description
WithHandlerCodec nil Sets the codec used for request/response serialization
WithHandlerGuard nil Sets a guard for request interception
WithHandlerLogger nil Sets the logger used by the handler

Example with custom handler options:

handler := httputil.NewHandler(
    myHandlerFunc,
    httputil.WithHandlerCodec(httputil.NewJSONCodec()),
    httputil.WithHandlerGuard(myAuthGuard),
    httputil.WithHandlerLogger(logger),
)

If handler options are not specified, the handler will inherit settings from the server when registered.

Response Helpers

The package provides helper functions for creating common HTTP responses:

// 200 OK
httputil.OK(data)

// 201 Created
httputil.Created(data)

// 202 Accepted
httputil.Accepted(data)

// 204 No Content
httputil.NoContent()

// 301/302/307/308 Redirects
httputil.Redirect(http.StatusTemporaryRedirect, "/new-location")

For custom status codes, use NewResponse:

httputil.NewResponse(http.StatusPartialContent, data)

Error Handling

RFC 7807 Problem Details

Error responses follow the RFC 7807 standard for Problem Details for HTTP APIs:

{
  "type": "https://example.com/problems/constraint-violation",
  "title": "Constraint Violation",
  "status": 400,
  "detail": "The request body contains invalid fields",
  "code": "INVALID_REQUEST_BODY",
  "instance": "/users",
  "invalid_params": [
    {
      "name": "email",
      "reason": "must be a valid email address"
    }
  ]
}
Predefined Error Types

The package provides predefined error constructors for common HTTP status codes:

// 400 Bad Request
problem.BadRequest("Invalid request format")

// 401 Unauthorized
problem.Unauthorized("Authentication required")

// 403 Forbidden
problem.Forbidden("Insufficient permissions")

// 404 Not Found
problem.NotFound("User not found")

// 409 Conflict
problem.ResourceExists("User already exists")

// 422 Unprocessable Entity
problem.ConstraintViolation("Invalid input", []problem.Parameter{
    {Name: "email", Reason: "must be a valid email address"},
})

// 500 Internal Server Error
problem.ServerError("An unexpected error occurred")

Middleware

Built-in Middleware

The package includes built-in middleware for common tasks:

  1. Panic Recovery - Automatically recovers from panics in handlers
  2. Max Body Size - Limits request body size to prevent abuse

These are applied automatically by the server.

Custom Middleware

You can create custom middleware using the MiddlewareFunc type:

func loggingMiddleware(logger *slog.Logger) httputil.MiddlewareFunc {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            start := time.Now()
            
            // Call the next handler
            next.ServeHTTP(w, r)
            
            // Log after the request is processed
            logger.InfoContext(r.Context(), "Request processed",
                slog.String("method", r.Method),
                slog.String("path", r.URL.Path),
                slog.Duration("duration", time.Since(start)),
            )
        })
    }
}

Apply middleware to endpoints:

endpoints := httputil.EndpointGroup{
    httputil.Endpoint{
        Method: http.MethodGet,
        Path:   "/users",
        Handler: httputil.NewHandler(listUsers),
    },
    httputil.Endpoint{
        Method: http.MethodPost,
        Path:   "/users",
        Handler: httputil.NewHandler(createUser),
    },
}

// Apply middleware to all endpoints in the group
server.Register(endpoints.WithMiddleware(loggingMiddleware(logger))...)

Guards

Guards provide a way to intercept and potentially modify requests before they reach handlers.

Request Interception

Implement the Guard interface:

type AuthGuard struct {
    secretKey string
}

func (g *AuthGuard) Guard(r *http.Request) (*http.Request, error) {
    token := r.Header.Get("Authorization")
    if token == "" {
        return nil, problem.Unauthorized("Missing authorization token")
    }
    
    // Validate token...
    
    // Add user info to context
    ctx := context.WithValue(r.Context(), "user", userInfo)
    return r.WithContext(ctx), nil
}

Apply the guard to an endpoint:

endpoint := httputil.NewEndpointWithGuard(
    httputil.Endpoint{
        Method: http.MethodGet,
        Path:   "/protected",
        Handler: httputil.NewHandler(protectedHandler),
    },
    &AuthGuard{secretKey: "your-secret-key"},
)
Guard Stacks

Combine multiple guards using GuardStack:

guards := httputil.GuardStack{
    &RateLimitGuard{},
    &AuthGuard{secretKey: "your-secret-key"},
    &LoggingGuard{logger: logger},
}

endpoint := httputil.NewEndpointWithGuard(
    httputil.Endpoint{
        Method: http.MethodGet,
        Path:   "/protected",
        Handler: httputil.NewHandler(protectedHandler),
    },
    guards,
)

Endpoint Groups

EndpointGroup allows you to manage multiple endpoints together:

userEndpoints := httputil.EndpointGroup{
    httputil.Endpoint{
        Method: http.MethodGet,
        Path:   "/users",
        Handler: httputil.NewHandler(listUsers),
    },
    httputil.Endpoint{
        Method: http.MethodPost,
        Path:   "/users",
        Handler: httputil.NewHandler(createUser),
    },
}

// Add a path prefix to all endpoints
prefixedEndpoints := userEndpoints.WithPrefix("/api/v1")

// Apply middleware to all endpoints
secureEndpoints := prefixedEndpoints.WithMiddleware(authMiddleware)

// Apply a guard to all endpoints
guardedEndpoints := secureEndpoints.WithGuard(&RateLimitGuard{})

// Register all endpoints
server.Register(guardedEndpoints...)

Testing

The package provides utilities for testing HTTP handlers:

func TestUserHandler(t *testing.T) {
    handler := httputil.NewHandler(func(r httputil.RequestEmpty) (*httputil.Response, error) {
        return httputil.OK(map[string]string{"message": "Hello, World!"})
    })
    
    req := httptest.NewRequest(http.MethodGet, "/users", nil)
    w := httptest.NewRecorder()
    
    handler.ServeHTTP(w, req)
    
    if w.Code != http.StatusOK {
        t.Errorf("Expected status %d, got %d", http.StatusOK, w.Code)
    }
    
    // Use the testutil package for JSON comparison
    expected := `{"message":"Hello, World!"}`
    if err := testutil.JSONEquals(expected, w.Body.String()); err != nil {
        t.Error(err)
    }
}

Examples

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.NewHandler(
                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 with 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.NewHandler(func(r httputil.RequestData[request]) (*httputil.Response, error) {
            return httputil.Created(response{Message: "Hello " + r.Data.Name + "!"})
        }),
    }
}
JSON Handler with Path Parameters
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.NewHandler(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.WrapNetHTTPHandlerFunc(
                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!"]
}
Advanced Examples
Combined Data and Parameters
func userEndpoint() httputil.Endpoint {
    type (
        params struct {
            ID string `path:"id" validate:"required,uuid"`
        }
        request struct {
            Name  string `json:"name" validate:"required"`
            Email string `json:"email" validate:"required,email"`
        }
    )

    return httputil.Endpoint{
        Method: http.MethodPut,
        Path:   "/users/{id}",
        Handler: httputil.NewHandler(func(r httputil.Request[request, params]) (*httputil.Response, error) {
            // Access both r.Data and r.Params
            return httputil.OK(map[string]string{
                "id": r.Params.ID,
                "name": r.Data.Name,
                "email": r.Data.Email,
            })
        }),
    }
}
Custom Middleware and Guards
func setupServer() *httputil.Server {
    logger := slogutil.NewJSONLogger()
    server := httputil.NewServer(logger)

    // Create middleware
    loggingMiddleware := func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            logger.InfoContext(r.Context(), "Request started", 
                slog.String("method", r.Method), 
                slog.String("path", r.URL.Path))
            next.ServeHTTP(w, r)
        })
    }

    // Create guard
    authGuard := httputil.GuardFunc(func(r *http.Request) (*http.Request, error) {
        token := r.Header.Get("Authorization")
        if token == "" {
            return nil, problem.Unauthorized("Missing authorization token")
        }
        return r, nil
    })

    // Create endpoints
    endpoints := httputil.EndpointGroup{
        httputil.Endpoint{
            Method: http.MethodGet,
            Path:   "/users",
            Handler: httputil.NewHandler(listUsers),
        },
        httputil.Endpoint{
            Method: http.MethodPost,
            Path:   "/users",
            Handler: httputil.NewHandler(createUser),
        },
    }

    // Apply middleware and guard
    secureEndpoints := endpoints.
        WithMiddleware(loggingMiddleware).
        WithGuard(authGuard).
        WithPrefix("/api/v1")

    server.Register(secureEndpoints...)
    
    return server
}

Client Usage

httputil.Client provides a convenient and idiomatic way to make HTTP requests to external services. It wraps the standard net/http.Client and offers simplified methods for common HTTP operations, along with robust response handling.

Creating a Client

You can create a new Client instance using httputil.NewClient and configure it with ClientOptions:

client := httputil.NewClient(
    httputil.WithClientBasePath("https://api.example.com"),
    httputil.WithClientCookieJar(nil), // Or provide a custom http.CookieJar.
    httputil.WithClientInterceptor(NewLogInterceptor(logger)), // Add middleware.
    httputil.WithClientTimeout(10 * time.Second),
)
defer client.Close()
Making Requests

The Client provides methods for common HTTP verbs. All methods return a *httputil.Result and an error.

// GET request
resp, err := client.Get(
	context.Background(), 
	"/users/123",
    httputil.WithRequestHeader("Authorization", "Bearer token"),
    httputil.WithRequestParam("version", "v1"),
)
if err != nil {
    fmt.Printf("Error making GET request: %v\n", err)
}

// POST request with a JSON body
type MyRequest struct {
    Name string `json:"name"`
}
reqBody := MyRequest{Name: "John Doe"}

resp, err = client.Post(context.Background(), "/users", reqBody)
if err != nil {
    fmt.Printf("Error making POST request: %v\n", err)
}

// PUT, PATCH, DELETE methods are similar
resp, err = client.Put(context.Background(), "/users/123", reqBody)
resp, err = client.Patch(context.Background(), "/users/123", reqBody)
resp, err = client.Delete(context.Background(), "/users/123")
Handling Responses

The *httputil.Result type wraps the *http.Response and provides convenient methods for checking status codes and decoding the response body.

type MyResponse struct {
    Message string `json:"message"`
}

// Check for success or error
if resp.IsSuccess() {
    var data MyResponse
    if err := resp.Decode(&data); err != nil {
        fmt.Printf("Error decoding success response: %v\n", err)
    }
	
    fmt.Printf("Success: %s\n", data.Message)
} else if resp.IsError() {
    // Decodes as RFC 7807 Problem Details
    problemDetails, err := resp.AsProblemDetails()
    if err != nil {
        fmt.Printf("Error decoding problem details: %v\n", err)
    }

    fmt.Printf("Error: %s - %s\n", problemDetails.Title, problemDetails.Detail)
} else {
    fmt.Printf("Unhandled status code: %d\n", resp.StatusCode)
}
Client Middleware with Interceptors

The client uses an interceptor model that wraps the underlying http.RoundTripper. Interceptors let you run logic before and after an HTTP request is sent (logging, retries, tracing, auth headers, metrics, etc.) without changing call sites. An interceptor has the shape:

type InterceptorFunc func(next http.RoundTripper) http.RoundTripper

Each interceptor receives the "next" RoundTripper and returns a new RoundTripper that calls next.RoundTrip(req) when appropriate. Interceptors are applied by wrapping the base transport so they form a chain: the first interceptor you provide becomes the outermost wrapper.

Basic rules and recommendations:

  • Keep interceptors small and focused (single responsibility).
  • Avoid modifying the incoming *http.Request in place; use req = req.WithContext(...) or req.Clone(...) when changing it.
  • Ensure you always call next.RoundTrip unless you intentionally short-circuit (for example, returning a cached response or an error).
  • Be mindful of retry/interceptor interactions (idempotency, body re-reads). If you need to retry requests with bodies, buffer them or use a replayable body.

Example: simple logging interceptor

func NewLogInterceptor(logger *slog.Logger) httputil.InterceptorFunc {
    return func(next http.RoundTripper) http.RoundTripper {
        return httputil.RoundTripperFunc(func(req *http.Request) (*http.Response, error) {
            start := time.Now()
            logger.DebugContext(
				req.Context(), 
				s"Client request started",
                slog.String("method", req.Method),
                slog.String("url", req.URL.String()),
            )

            resp, err := next.RoundTrip(req)

            logger.InfoContext(
				req.Context(), 
				"Client request completed",
                slog.String("method", req.Method),
                slog.String("url", req.URL.String()),
                slog.Int("status", resp.StatusCode),
                slog.Duration("duration", time.Since(start)),
                slog.Any("error", err),
            )
			
            return resp, err
        })
    }
}
Client Options

httputil.NewClient accepts ClientOptions to customize the underlying http.Client:

Option Default Description
WithClientBasePath "" Sets a base URL path for all requests
WithClientCodec JSON Sets the codec for request/response serialization
WithClientCookieJar nil Sets the http.CookieJar for the client
WithClientInterceptor http.DefaultTransport Wraps the http.DefaultTransport to provide client middleware.
WithClientTimeout 60s Sets the total timeout for requests
WithClientRedirectPolicy nil Sets the redirect policy for the client
Request Options

Request-specific options can be passed to individual HTTP method calls:

Option Description
WithRequestHeader Adds a single HTTP header to the request
WithRequestHeaders Adds multiple HTTP headers from a map
WithRequestParam Adds a single query parameter to the request
WithRequestParams Adds multiple query parameters from a map

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

Type Safety with Generics

The package uses Go generics to provide type-safe request handling, ensuring that request data and parameters are properly typed.

Graceful Shutdown

The server implementation includes graceful shutdown handling, ensuring that in-flight requests are completed before the server stops.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

License

This project is licensed under the MIT License - see the LICENSE file for details.

Documentation

Overview

Package httputil provides utilities for building HTTP clients and servers.

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

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.

func NewHandler added in v0.2.0

func NewHandler[D, P any](action Action[D, P], options ...HandlerOption) http.Handler

NewHandler creates a new Handler that wraps the provided Action. It accepts options to configure the handler's behavior.

func WrapNetHTTPHandler added in v0.2.0

func WrapNetHTTPHandler(h http.Handler) http.Handler

WrapNetHTTPHandler wraps a standard http.Handler with additional functionality like optional guard and logging.

func WrapNetHTTPHandlerFunc added in v0.2.0

func WrapNetHTTPHandlerFunc(h http.HandlerFunc) http.Handler

WrapNetHTTPHandlerFunc wraps an http.HandlerFunc in a netHTTPHandler to support additional features like guarding and logging.

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 Client added in v0.2.0

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

Client is an HTTP client that wraps a standard http.Client and provides convenience methods for making requests and handling responses.

func NewClient added in v0.2.0

func NewClient(options ...ClientOption) *Client

NewClient creates a new Client with the given options.

func (*Client) BasePath added in v0.2.0

func (c *Client) BasePath() string

BasePath returns the base path for the Client.

func (*Client) Client added in v0.2.0

func (c *Client) Client() *http.Client

Client returns the underlying *http.Client.

func (*Client) Close added in v0.2.0

func (c *Client) Close() error

Close closes any connections on its http.Client.Transport which were previously connected from previous requests but are now sitting idle in a "keep-alive" state. It does not interrupt any connections currently in use. See the http.Client.CloseIdleConnections documentation for details.

If http.Client.Transport does not have a http.Client.CloseIdleConnections method then this method does nothing. Interestingly, the http.Client type does not implement the io.Closer interface. WithClientInterceptor wraps the http.Client.Transport to ensure that the CloseIdleConnections method is called.

func (*Client) Delete added in v0.2.0

func (c *Client) Delete(ctx context.Context, path string, options ...RequestOption) (*Result, error)

Delete sends an HTTP DELETE request to the specified path. It returns a Result which wraps the http.Response, or an error.

func (*Client) Do added in v0.2.0

func (c *Client) Do(req *http.Request) (*http.Response, error)

Do executes the provided request using the Client's underlying *http.Client. It returns the raw *http.Response and an error, if any.

func (*Client) Get added in v0.2.0

func (c *Client) Get(ctx context.Context, path string, options ...RequestOption) (*Result, error)

Get sends an HTTP GET request to the specified path. It returns a Result which wraps the http.Response, or an error.

func (*Client) Patch added in v0.2.0

func (c *Client) Patch(ctx context.Context, path string, body any, options ...RequestOption) (*Result, error)

Patch sends an HTTP PATCH request to the specified path with the given body. It returns a Result which wraps the http.Response, or an error.

func (*Client) Post added in v0.2.0

func (c *Client) Post(ctx context.Context, path string, body any, options ...RequestOption) (*Result, error)

Post sends an HTTP POST request to the specified path with the given body. It returns a Result which wraps the http.Response, or an error.

func (*Client) Put added in v0.2.0

func (c *Client) Put(ctx context.Context, path string, body any, options ...RequestOption) (*Result, error)

Put sends an HTTP PUT request to the specified path with the given body. It returns a Result which wraps the http.Response, or an error.

type ClientCodec added in v0.2.0

type ClientCodec interface {
	// ContentType returns the Content-Type header value for the client codec.
	ContentType() string
	// Encode encodes the given data into a new io.Reader.
	Encode(data any) (io.Reader, error)
	// Decode reads and decodes the response body into the provided target struct
	Decode(r io.Reader, into any) error
}

ClientCodec is an interface for encoding and decoding HTTP request and response bodies for the client. It provides methods for encoding request data and decoding response data or errors.

type ClientOption added in v0.2.0

type ClientOption func(co *clientOptions)

ClientOption allows default doer config values to be overridden.

func WithClientBasePath added in v0.2.0

func WithClientBasePath(basePath string) ClientOption

WithClientBasePath sets the base path for the Client. This is used to prefix relative paths in the request URLs.

func WithClientCodec added in v0.2.0

func WithClientCodec(codec ClientCodec) ClientOption

WithClientCodec sets the ClientCodec that the Client will use when making requests.

func WithClientCookieJar added in v0.2.0

func WithClientCookieJar(jar http.CookieJar) ClientOption

WithClientCookieJar sets the CookieJar that the Client will use when making requests.

func WithClientInterceptor added in v0.2.0

func WithClientInterceptor(intercept InterceptorFunc) ClientOption

WithClientInterceptor adds an InterceptorFunc to the Client. Each InterceptorFunc will be executed in the order that it was added.

func WithClientRedirectPolicy added in v0.2.0

func WithClientRedirectPolicy(policy RedirectPolicy) ClientOption

WithClientRedirectPolicy sets the RedirectPolicy that the Client will use when following redirects.

func WithClientTimeout added in v0.2.0

func WithClientTimeout(timeout time.Duration) ClientOption

WithClientTimeout sets the timeout for the doer. This is the maximum amount of time the doer will wait for a response from the server.

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 [http.Handler] that will handle requests for this endpoint.
	Handler http.Handler
	// contains filtered or unexported fields
}

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

func NewEndpointWithGuard added in v0.2.0

func NewEndpointWithGuard(e Endpoint, g Guard) Endpoint

NewEndpointWithGuard associates the given Guard with the specified Endpoint. It returns a new Endpoint with the Guard 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) WithGuard added in v0.2.0

func (eg EndpointGroup) WithGuard(g Guard) EndpointGroup

WithGuard adds the Guard as a GuardStack with the currently set Guard as the second Guard in the stack. It returns a new slice of EndpointGroup with the Guard set. The original endpoints are not modified.

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.

type Guard added in v0.2.0

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

Guard defines an interface for components that protect access to a Handler's Action. It acts as a crucial pre-processing gatekeeper within the handler's request lifecycle, executing after the request has been routed to the handler, but *before* any automatic request body decoding or parameter binding occurs.

Its primary role is to enforce preconditions, such as authentication, authorization, API key validation, or other checks based on request headers, context, or basic properties, before allowing the request to proceed to the core business logic (the Action).

type GuardFunc added in v0.2.0

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

GuardFunc 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 (GuardFunc) Guard added in v0.2.0

func (rif GuardFunc) Guard(r *http.Request) (*http.Request, error)

Guard applies the GuardFunc to modify or inspect the provided HTTP request.

type GuardStack added in v0.2.0

type GuardStack []Guard

GuardStack represents multiple Guard instances that will be run in order.

func (GuardStack) Guard added in v0.2.0

func (gs GuardStack) Guard(r *http.Request) (*http.Request, error)

Guard will run each Guard 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 HandlerOption added in v0.2.0

type HandlerOption func(ho *handlerOptions)

HandlerOption allows default handler config values to be overridden.

func WithHandlerCodec added in v0.2.0

func WithHandlerCodec(codec ServerCodec) HandlerOption

WithHandlerCodec sets the ServerCodec that the Handler will use when NewHandler is called.

func WithHandlerGuard added in v0.2.0

func WithHandlerGuard(guard Guard) HandlerOption

WithHandlerGuard sets the Guard that the Handler will use when NewHandler is called.

func WithHandlerLogger added in v0.2.0

func WithHandlerLogger(logger *slog.Logger) HandlerOption

WithHandlerLogger sets the slog.Logger that the Handler will use when NewHandler is called.

type InterceptorFunc added in v0.2.0

type InterceptorFunc func(next http.RoundTripper) http.RoundTripper

InterceptorFunc defines a function type for HTTP client middleware. An InterceptorFunc takes an http.RoundTripper as input and returns a new http.RoundTripper that wraps the original action with additional logic.

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 JSONClientCodec added in v0.2.0

type JSONClientCodec struct{}

JSONClientCodec provides methods to encode data as JSON or decode data from JSON in HTTP requests and responses.

func NewJSONClientCodec added in v0.2.0

func NewJSONClientCodec() JSONClientCodec

NewJSONClientCodec creates a new JSONClientCodec instance.

func (JSONClientCodec) ContentType added in v0.2.0

func (c JSONClientCodec) ContentType() string

ContentType returns the Content-Type header value for JSON requests and responses.

func (JSONClientCodec) Decode added in v0.2.0

func (c JSONClientCodec) Decode(r io.Reader, into any) error

Decode reads and decodes the JSON body of an HTTP response into the provided target struct or variable.

func (JSONClientCodec) Encode added in v0.2.0

func (c JSONClientCodec) Encode(data any) (io.Reader, error)

Encode encodes the given data into a new io.Reader.

type JSONServerCodec added in v0.2.0

type JSONServerCodec struct{}

JSONServerCodec provides methods to encode data as JSON or decode data from JSON in HTTP requests and responses.

func NewJSONServerCodec added in v0.2.0

func NewJSONServerCodec() JSONServerCodec

NewJSONServerCodec creates a new JSONServerCodec instance.

func (JSONServerCodec) Decode added in v0.2.0

func (c JSONServerCodec) Decode(r *http.Request, into any) error

Decode reads and decodes the JSON body of an HTTP request into the provided target struct or variable. Returns an error if decoding fails or if the request body is nil.

func (JSONServerCodec) Encode added in v0.2.0

func (c JSONServerCodec) Encode(w http.ResponseWriter, data any) error

Encode writes the given data as JSON to the provided HTTP response writer with the appropriate Content-Type header.

func (JSONServerCodec) EncodeError added in v0.2.0

func (c JSONServerCodec) EncodeError(w http.ResponseWriter, err error) error

EncodeError encodes an error into an HTTP response, handling `problem.DetailedError` if applicable to set the correct content type, or falling back to standard JSON encoding otherwise.

type MiddlewareFunc

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

MiddlewareFunc defines a function type for HTTP server 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 RedirectPolicy added in v0.2.0

type RedirectPolicy func(req *http.Request, via []*http.Request) error

RedirectPolicy defines the policy for handling HTTP redirects.

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 http.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. It's a type alias for Request with a generic data type D and an empty struct for Params. Use this type when your handler needs to process request body data but doesn't need URL parameters.

type RequestEmpty

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

RequestEmpty represents an empty Request that expects no Params or data. It's a type alias for Request with empty structs for both data and Params. Use this type when your handler doesn't need to process any request body or URL parameters.

type RequestOption added in v0.2.0

type RequestOption func(ro *requestOptions)

RequestOption allows default request config values to be overridden.

func WithRequestHeader added in v0.2.0

func WithRequestHeader(k, v string) RequestOption

WithRequestHeader adds a header to the request.

func WithRequestHeaders added in v0.2.0

func WithRequestHeaders(headers map[string]string) RequestOption

WithRequestHeaders adds multiple headers to the request.

func WithRequestParam added in v0.2.0

func WithRequestParam(k, v string) RequestOption

WithRequestParam adds a query parameter to the request.

func WithRequestParams added in v0.2.0

func WithRequestParams(params map[string]string) RequestOption

WithRequestParams adds multiple query parameters to the request.

type RequestParams

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

RequestParams represents a Request that expects Params but no data. It's a type alias for Request with an empty struct for data and a generic Params type P. Use this type when your handler needs to process URL parameters but doesn't need request body 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 Guard 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 Result added in v0.2.0

type Result struct {
	*http.Response
	// contains filtered or unexported fields
}

Result wraps an http.Response and provides convenience methods for decoding the response body and checking status codes.

func (*Result) AsProblemDetails added in v0.2.0

func (r *Result) AsProblemDetails() (*problem.DetailedError, error)

AsProblemDetails attempts to decode the response body into a problem.DetailedError. This is useful for handling API errors that conform to RFC 7807.

Note: This method consumes the response body. Subsequent calls to Decode or AsProblemDetails will fail if the body has already been read.

func (*Result) Decode added in v0.2.0

func (r *Result) Decode(into any) (err error)

Decode decodes the response body into the provided target. It uses the ClientCodec to perform the decoding.

Note: This method consumes the response body. Subsequent calls to Decode or AsProblemDetails will fail if the body has already been read.

func (*Result) IsError added in v0.2.0

func (r *Result) IsError() bool

IsError returns true if the HTTP status code is 400 or greater.

func (*Result) IsSuccess added in v0.2.0

func (r *Result) IsSuccess() bool

IsSuccess returns true if the HTTP status code is between 200 and 299 (inclusive).

type RoundTripperFunc added in v0.2.0

type RoundTripperFunc func(req *http.Request) (*http.Response, error)

RoundTripperFunc is an adapter to allow the use of ordinary functions as http.RoundTripper. If f is a function with the appropriate signature, RoundTripperFunc(f) is a http.RoundTripper that calls f.

func (RoundTripperFunc) RoundTrip added in v0.2.0

func (f RoundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error)

RoundTrip implements the http.RoundTripper interface.

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 allow for customization of server settings such as the address, codec, 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 ServerCodec added in v0.2.0

type ServerCodec interface {
	// Decode decodes the request data and sets it on into. Implementations of
	// Decode should return [io.EOF] if the request data is empty when Decode is
	// called.
	Decode(r *http.Request, into any) error
	// Encode writes the given data to the http.ResponseWriter after encoding it,
	// returning an error if encoding fails.
	Encode(w http.ResponseWriter, data any) error
	// EncodeError encodes the provided error into the HTTP response writer and
	// returns an error if encoding fails.
	EncodeError(w http.ResponseWriter, err error) error
}

ServerCodec is an interface for encoding and decoding HTTP requests and responses. It provides methods for decoding request data and encoding response data or errors.

type ServerOption

type ServerOption func(so *serverOptions)

ServerOption allows default server config values to be overridden.

func WithServerAddress added in v0.2.0

func WithServerAddress(address string) ServerOption

WithServerAddress sets the address that the Server will listen to and serve on.

func WithServerCodec added in v0.2.0

func WithServerCodec(codec ServerCodec) ServerOption

WithServerCodec sets the ServerCodec that the Server will use by default when NewHandler is called.

func WithServerIdleTimeout added in v0.2.0

func WithServerIdleTimeout(timeout time.Duration) ServerOption

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

func WithServerMaxBodySize added in v0.2.0

func WithServerMaxBodySize(size int64) ServerOption

WithServerMaxBodySize 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 WithServerReadHeaderTimeout added in v0.2.0

func WithServerReadHeaderTimeout(timeout time.Duration) ServerOption

WithServerReadHeaderTimeout 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 WithServerReadTimeout added in v0.2.0

func WithServerReadTimeout(timeout time.Duration) ServerOption

WithServerReadTimeout 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 WithServerShutdownTimeout added in v0.2.0

func WithServerShutdownTimeout(timeout time.Duration) ServerOption

WithServerShutdownTimeout 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 WithServerWriteTimeout added in v0.2.0

func WithServerWriteTimeout(timeout time.Duration) ServerOption

WithServerWriteTimeout 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