httpwrap

package module
v0.0.0-...-83f4b73 Latest Latest
Warning

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

Go to latest
Published: May 25, 2025 License: MIT-0 Imports: 8 Imported by: 0

README

httpwrap


Go Reference Unit Tests Workflow

httpwrap is a thin wrapper around the default http library that lets you compose handlers and automatically inject outputs into the inputs of the next handler.

The idea is that you can write your http endpoints as you would regular functions, and have httpwrap automatically populate those structures from the incoming http requests.

This goes against most of the popular Go libraries that rely on an opaque and type-unsafe Context to pass the relevant information to the business logic of your application.

Simple API Example

type ListMoviesParams struct {
    // This will be populated from the cookies sent with the request.
    UserID string `http:"cookie=x-user-id"`
    
    // This will be populated from the URL query parameters that are 
    // sent with the request.
    ReleaseYear int `http:"query=release-year"`
    
    // The rest, by default, will be parsed from the body of the request 
    // interpreted as JSON.
    Director *string
    Actor string
}

type ListMoviesResponse struct {
    Movies []string `json:"movies"`
}

// The response and the error will automatically be written to the stdlib http.ResponseWriter. By
// default, the response will be JSON marshalled and the status code will be 200 OK.
func ListMovies(params ListMoviesParams) (ListMoviesResponse, error) {
    if params.ReleaseYear != 2022 {
        return httpwrap.NewHTTPError(http.StatusBadRequest, "Only 2022 movies are searchable.")
    }
	
    ...
		
    return ListMoviesResponse{
        Movies: []string{
            "Finding Nemo",
            "Good Will Hunting",
        },
    }
}

Raw HTTP Access

For certain endpoints or applications, it can be desirable to forego the automatic sending of the response or error with JSON. The example below shows how this is done, which looks pretty much identical to vanilla Go:

func RawHTTPHandler(rw http.ResponseWriter, req *http.Request) error { 
    if req.Origin != "..." {
        return httpwrap.NewHTTPError(http.StatusUnauthorized, "Bad origin.")
    }
    rw.WriteHeader(201)
    rw.Write([]byte{"Raw HTTP body here"})
    return nil
}

Routing

Routing with httpwrap is nearly identical as you would otherwise do it. You can either use the standard lib or any other routing libraries that you are used to. The following snippet uses gorilla/mux:

import (
    "log"
    "net/http"

    "github.com/apourchet/httpwrap"
    "github.com/gorilla/mux"
)

func main() {
    // Tell the httpwrapper to run checkAPICreds as middleware before moving on to call
    // the endpoints themselves.
    httpWrapper := httpwrap.NewStandardWrapper().Before(checkAPICreds)

    // Using gorilla/mux for this example, but httpWrapper.Wrap will turn your regular endpoint
    // functions into the required http.HandlerFunc type.
    router := mux.NewRouter()
    router.Handle("/movies/list", httpWrapper.Wrap(ListMovies)).Methods("GET")
    router.Handle("/raw-handler", httpWrapper.Wrap(RawHTTPHandler)).Methods("GET")
    http.Handle("/", router)
	
    log.Fatal(http.ListenAndServe(":3000", router))
}

type APICredentials struct {
    Key string `http:"header=x-application-passcode"`
}

...

// checkAPICreds checks the api credentials passed into the request. Those APICredentials
// will be populated using the headers in the http request.
func checkAPICreds(creds APICredentials) error {
    if creds.Key == "my-secret-key" {
        return nil
    }
    return httpwrap.NewHTTPError(http.StatusForbidden, "Bad credentials.")
}

This example also displays a simple authorization middleware, checkAPICreds.

Middleware

Middlewares can be used to either short-circuit the http request lifecycle and return early, or to provide additional information to the endpoint that gets called after it. The following example uses two separate middleware functions to accomplish both.

import (
    "log"
    "net/http"

    "github.com/apourchet/httpwrap"
    "github.com/gorilla/mux"
)

func main() {
    httpWrapper := httpwrap.NewStandardWrapper().
        Before(getUserAccountInfo)
    wrapperWithAccessCheck := httpWrapper.Before(ensureUserAccess)
	
    // The listMovies endpoint needs account information only, but the addMovies endpoint also performs
    // an access list check.
    router := mux.NewRouter()
    router.Handle("/movies/list", httpWrapper.Wrap(ListMovies)).Methods("GET")
    router.Handle("/movies/add", wrapperWithAccessCheck.Wrap(AddMovie)).Methods("PUT")
    http.Handle("/", router)

    log.Fatal(http.ListenAndServe(":3000", router))
}

type UserAuthenticationMaterial struct {
	BearerToken string `http:"header=Authorization"`
}

func getUserAccountInfo(authMaterial UserAuthenticationMaterial) (UserAccountInfo, error) {
    userId, err := decodeBearerToken(authMaterial.BearerToken)
    if err != nil {
        return httpwrap.NewHTTPError(http.StatusUnauthorized, "Bad authentication material.")
    }

    // Find the user information in the database for instance.
    accountInfo, err := database.FindUserInformation(userId)
    if err != nil {
        return err
    }
	
    ...
	
    return UserAccountInfo{
        UserID: userId,
        UserHasAccess: false,
    }, nil
}

func ensureUserAccess(accountInfo UserAccountInfo) error {
    if !accountInfo.UserHasAccess {
        // Returning an error from a middleware will short-circuit the rest of the request lifecycle and 
        // early-return this to the client.
        return httpwrap.NewHTTPError(http.StatusForbidden, "Access forbidden.")
    }
    return nil
}

// The two endpoints below will have access not only to the information provided by the middlewares, but
// can gather additional parameters from the http request by taking in extra arguments.
// NOTE: These two endpoints do not _have to_ take in the information from the middleware, so if accountInfo
// is not actually used in the endpoint, it can be omitted from the function signature altogether.
func ListMovies(accountInfo UserAccountInfo, params ListMoviesParams) (ListMoviesResponse, error) {
    ...
}

func AddMovie(accountInfo UserAccountInfo, params AddMovieParams) (AddMovieResponse, error) {
    ...
}

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type DecodeFunc

type DecodeFunc func(req *http.Request, obj any) error

DecodeFunc is the function signature for decoding a request into an object.

type Decoder

type Decoder struct {
	// DecodeBody is the function that will be used to decode the
	// request body into a target object.
	DecodeBody DecodeFunc

	// Header is the function used to get the string value of a header.
	Header func(*http.Request, string) (string, error)

	// Segment is the function used to get the string value of a path
	// parameter.
	Segment func(*http.Request, string) (string, error)

	// Queries is the function used to get the string values of a query
	// parameter.
	Queries func(*http.Request, string) ([]string, error)

	// Cookie is the function used to get the value of a cookie from a
	// request.
	Cookie func(*http.Request, string) (string, error)
}

Decoder is a struct that allows for the decoding of http requests into arbitrary objects.

func NewDecoder

func NewDecoder() *Decoder

NewDecoder returns a new decoder with sensible defaults for the DecodeBody, Header and Query functions. By default, it uses a JSON decoder on the request body.

func (*Decoder) Decode

func (d *Decoder) Decode(req *http.Request, obj any) error

Decode will (by default), given a struct definition:

type Request struct {
		AuthString string    `http:"header=Authorization"`
		Limit int            `http:"query=limit"`
		Resource string      `http:"segment=resource"`
		UserCookie float64   `http:"cookie=user_cookie"`
		Extra map[string]int `json:"extra"`
}

The Authorization header will be parsed into the field Token of the request struct.

The Limit field will come from the query string.

The Resource field will come from the resource value of the path (e.g: /api/pets/{resource}).

The Extra field will come from deserializing the request body from JSON encoding.

type HTTPError

type HTTPError interface {
	error
	HTTPResponse
}

func NewHTTPError

func NewHTTPError(code int, format string, args ...any) HTTPError

func NewNoopError

func NewNoopError() HTTPError

NewNoopError returns an HTTPError that will completely bypass the deserialization logic. This can be used when the endpoint or middleware operates directly on the native http.ResponseWriter.

type HTTPResponse

type HTTPResponse interface {
	StatusCode() int
	WriteBody(io.Writer) error
}

HTTPResponse is used by the StandardResponseWriter to construct the response body according to the StatusCode() and WriteBody() functions. If the StatusCode() function returns `0`, the StandardResponseWriter will assume that WriteHeader has already been called on the http.ResponseWriter object.

func NewJSONResponse

func NewJSONResponse(code int, body any) HTTPResponse

type RequestReader

type RequestReader func(http.ResponseWriter, *http.Request, any) error

RequestReader is the function signature for unmarshalling a *http.Request into an object.

func StandardRequestReader

func StandardRequestReader() RequestReader

The StandardRequestReader decodes the request using the following:

- Cookies

- Query Params

- Request Headers

- Request Path Segment (e.g: /api/pets/{id})

- JSON Decoding of the http request body

type ResponseWriter

type ResponseWriter func(w http.ResponseWriter, r *http.Request, res any, err error)

ResponseWriter is the function signature for marshalling a structured response into a standard http.ResponseWriter

func StandardResponseWriter

func StandardResponseWriter() ResponseWriter

StandardResponseWriter will try to cast the error and response objects to the HTTPResponse interface and use them to send the response to the client. By default, it will send a 200 OK and encode the response object as JSON. If the HTTPResponse has a `0` StatusCode, WriteHeader will not be called. If the error is not an HTTPResponse, a 500 status code will be returned with the body being exactly the error's string.

type Wrapper

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

Wrapper implements the http.Handler interface, wrapping the handlers that are passed in.

func New

func New() Wrapper

New creates a new Wrapper object. This wrapper object will not interact in any way with the http request and response writer.

func NewStandardWrapper

func NewStandardWrapper() Wrapper

NewStandardWrapper returns a new wrapper using the StandardRequestReader and the StandardResponseWriter.

func (Wrapper) Before

func (w Wrapper) Before(fns ...any) Wrapper

Before adds a new function that will execute before the main handler. The chain of befores will end if a before returns a non-nil error value.

func (Wrapper) Finally

func (w Wrapper) Finally(fn any) Wrapper

Finally sets the last function that will execute during a request. This function gets invoked with the response object and the possible error returned from the main endpoint function.

func (Wrapper) WithRequestReader

func (w Wrapper) WithRequestReader(cons RequestReader) Wrapper

WithRequestReader returns a new wrapper with the given RequestReader function.

func (Wrapper) Wrap

func (w Wrapper) Wrap(fn any) http.Handler

Wrap sets the main handling function to process requests. This Wrap function must be called to get an `http.Handler` type.

Directories

Path Synopsis
examples
simple command
standard command

Jump to

Keyboard shortcuts

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