webmux

package module
v0.0.0-...-bda4419 Latest Latest
Warning

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

Go to latest
Published: Jan 27, 2024 License: BSD-3-Clause Imports: 8 Imported by: 0

README

webmux

GoDoc Status

Package webmux provides a HTTP request multiplexer for Go web servers.

Features

  • Performant: Very efficient, very fast
  • Flexible: Match on methods, named parameters (/users/:id), and wildcards (/img/*)
  • Correct: Handles HEAD and OPTIONS requests, 405 responses, etc.
  • Compatible: Compatible with net/http requests, responses, and handlers

Installation

go get -u go.destructure.dev/webmux

Usage

Quick start

The following example creates a new ServeMux, adds a handler function that takes a parameter, and starts a web server:

mux := webmux.New()

greet := func(w http.ResponseWriter, r *http.Request) error {
    m, _ := webmux.FromContext(r.Context())

    name := m.Param("name")

    _, err := fmt.Fprintf(w, "Hello %s!", name)

    return err
}

mux.HandleFunc(http.MethodGet, "/greet/:name", greet)

log.Fatal(http.ListenAndServe(":3030", mux))

A full runnable example showing the necessary imports is available in the _examples directory.

Registration

The multiplexer dispatches requests to handlers based on a HTTP method and URL path pattern.

A handler is registered using the Handle or HandleFunc methods, like this:

mux.Handle(http.MethodGet, "/greet/:name", h)

mux.HandleFunc(http.MethodGet, "/greet/:name", func(w http.ResponseWriter, r *http.Request) error {
    // ...
})

When to use Handle vs. HandleFunc is explained in the Handlers section.

The first argument is a HTTP method. The second argument is a URL path to match, which may contain dynamic path segments. The third argument is the handler or handler function.

Matching methods

The method is a HTTP method such as GET, POST, or DELETE. Typically methods are provided using the net/http constants.

To respond to multiple methods with the same handler, use HandleMethods or HandleMethodsFunc instead.

The first argument must be a MethodSet. A MethodSet can be easily constructed using the webmux.Methods function.

mux.HandleMethods(webmux.Methods(http.MethodGet, http.MethodPost), "/users", h)

To handle any of the common HTTP methods (this includes all of the methods defined by the net/http constants), use the webmux.AnyMethods function:

mux.HandleMethods(webmux.AnyMethod(), "/users", h)

HTTP allows defining your own methods. For example, WebDAV uses methods such as COPY and LOCK. Because the method is just a string (and a method set is a set of strings) this is fully supported.

If a request matches a path but not a method, a 405 "Method Not Allowed" response should be returned -- not a 404 "Not Found". The default error handler does this automatically and includes the necessary Allow header.

Matching paths

The path pattern matches the URL path, using a subset of the browser's URL Pattern API syntax.

Patterns can contain:

  • Literal strings which will be matched exactly.
  • Wildcards (/posts/*) that match any character.
  • Named groups (/posts/:id) which extract a part of the matched URL.

The simplest match is a literal match of an exact path:

mux.Handle(http.MethodGet, "/users", h)

The pattern /users would only match /users. It would not match /users/new. Any trailing slash is ignored, so a request for /users/ is interpreted identically to a request for /users.

A wildcard or named group may be used to match one or more path segments containing arbitrary strings.

mux.Handle(http.MethodGet, "/users/:id", h)

The pattern /users would match /users/1, /users/matt, etc. It would not match /users/1/settings or /users. When the placeholder is specified with a colon (:) the pattern only matches characters until the next slash (/).

To greedily match one or more segments until the end of the path, use an asterisk (*) instead:

mux.Handle(http.MethodGet, "/users/*", h)

The pattern /users/* would match /users/1, users/1/settings, etc. It would not match /users, because at least one segment must be matched by the wildcard.

Wildcards may be named:

mux.Handle(http.MethodGet, "/users/*rest", h)

This can be useful when extracting the parameter value as explained below.

Match priority

It can be useful to register patterns that overlap. Consider the following patterns for a hypothetical application:

  • /users/new
  • /users/:id
  • /*

In this example /users/:id should match paths like /users/1, /users/new should only match that literal path, and /* should match anything else (typically used to fallback to a Single Page Application).

These patterns match like you would expect. The more exact match is always prioritized over the less exact match. Knowing that, /users/new matches over /users/:id, and /users/:id matches over /*.

Match parameters

When a pattern is matched the path segments corresponding to each match are captured. To access a parameter, first retrieve the MuxMatch from the Request context:

h := func(w http.ResponseWriter, r *http.Request) error {
    match, ok := webmux.FromContext(r.Context())

    // ...
}

The ok return value will always be true within a handler, and the match will not be nil.

Next, retrieve the parameter by calling MuxMatch.Param with the parameter's name:

userID := m.Param("id") 

The parameter is always a string. It captures everything between the path segments where the parameter appears, or from the start of the path segment to the end of the path if the parameter is a wildcard (*).

If a parameter with the given name was not captured, Param returns the empty string.

To access all parameters as a slice, call Params instead:

params := m.Params() 

Once the slice is retrieved, you can access parameters by position. This is useful when parameters are un-named, which is common for wildcards. For example, when matching /assets/*, you would get the value corresponding to the wildcard like this:

params := m.Params() 

filepath := params[0]
Handlers

The quick start example used a function or "HandlerFunc". A HandlerFunc is just an adapter for implementing the Handler interface, which looks like this:

type Handler interface {
	ServeHTTPErr(http.ResponseWriter, *http.Request) error
}

Use ServeMux.Handle to register a Handler, and ServeMux.HandleFunc to register a HandlerFunc.

Stdlib handlers

The net/http package in the standard library defines the following Handler interface:

type Handler interface {
	ServeHTTP(http.ResponseWriter, *http.Request)
}

It's nearly identical to ours, but does not allow returning an error. This is incredibly inconvenient when you want to handle errors in one place and leads to a lot of boilerplate. However, a lot of useful packages are compatible with this interface.

To adapt a stdlib compatible Handler, use the FallibleFunc function like so:

h := webmux.FallibleFunc(h)

The error returned by h will always be nil.

HEAD requests

Responses to HEAD requests must return the response headers as if a GET request had been made, but without returning a body.

Unless a HEAD handler is registered, the GET handler will be called for HEAD requests. It is not necessary to do anything different in the handler, as the default http.ResponseWriter will omit the body but write the Content-Length header.

When sending a large file of a known length it can be more efficient to check the request method in the handler, then only write the Content-Length header.

OPTION requests

By default OPTION requests are handled by sending a 204 No Content response and setting the Allow header. This does not take precendence over not found responses.

This behavior can be overriden by explicitly registering a handler for the OPTION method.

Error handling

When a handler returns an error the error handler is called. The error handler is responsible for sending an appropriate response to the client and potentially reporting the error.

The default error handler returns "Internal Server Error" in plain text with a 500 status code. You will likely want to override this.

To override the error handler use ServeMux.HandleError or ServeMux.HandleErrorFunc:

mux.HandleErrorFunc(func(w http.ResponseWriter, r *http.Request, err error) {
    // ...

	log.Printf("error: %s", err.Error())

    code := http.StatusInternalServerError

	http.Error(w, http.StatusText(code), code)
})

The error handler should also handle ErrMuxNotFound errors; see below.

Not found errors

When a handler is not found an ErrMuxNotFound error is returned. The error handler can then return an appropriate response to the client.

The default error handler provides an example of handling the not found error correctly:

if errors.Is(err, ErrMuxNotFound) {
    match, ok := FromContext(r.Context())

    if !ok {
        writeError(w, http.StatusNotFound)
        return
    }

    w.Header().Add("Allow", match.Methods().String())
    writeError(w, http.StatusMethodNotAllowed)

    return
}

// ...

Of note is that a 405 Method Not Allowed response is returned with the Allow header if the pattern matched but a handler was not bound for the request method. Otherwise a 404 Not found error is returned.

FAQ

Why another router?

There weren't any other routers that hit on all the right features.

Before Go 1.22 net/http couldn't match path patterns. Now it can but it made a lot of compromises to keep backwards compatibility. Because of those compromises it's API is confusing and error prone, and the implementation cannot be efficient.

The matching logic for a lot of third party routers is complicated. And complex matching rules slow down every single request. Some routers dont' allow "conflicting" routes like /users/:id and /users/new, when intuitively you would think that should be allowed, with the exact match taking priority. Others handle trailing and duplicate slashes in inconsistent ways when they shouldn't really matter. Some depend on the order the routes were registered in the code to determine priority.

Handling OPTIONS and HEAD requests correctly is important for APIs but most routers don't. A related and often overlooked issue is sending the Allow header in 405 responses. The router has to be designed for this up front or the method lookup will be slow, which is a problem for APIs where lots of requests get preflighted by the browser.

Non-Features

There are a lot of features other routers have that aren't present in this package. Most (all?) of these were intentionally omitted.

Regex parameters

Regex parameters refers to route parameters of the form /user/(\\d+). Regex parameters make the matching process much slower, even if the route that finally matches did not contain a regex parameter. If the regex does not match the user gets a 404 Not Found error with no context to understand what happened.

Instead of regex parameters, use normal parameters and validate the value within the handler. In the handler you can return a more useful error.

Partial segment matching

You can't match parts of a segment as separate parameters, like /articles/{month}-{day}-{year}. This is rarely useful for matching; just match on the whole segment and parse it within the handler.

Named routes

Some routers let you assign a name to a route, like users.update for PATCH /users/:id. You can then do "reverse routing", generating a URL by providing the name and parameters.

Calling RouteURL("users.update", 1) is not much easier than /users/+strconv.itoa(1) and it's less clear.

Route groups

This feature is commonly used for "RESTful" JSON APIs:

r.Route("/articles", func(r Router) {
    r.Get("/", listArticles) // GET /articles
    r.Get("/:id", showArticle) // GET /articles/:id
})

It's slightly more convenient when writing but everyone that reads it has to re-assemble the path in their head. With more than one level of nesting it's a total mess.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrMuxNotFound = errors.New("mux match not found")

ErrMuxNotFound is returned by ServeMux when a matching handler was not found.

Functions

func NewContext

func NewContext(ctx context.Context, m *MuxMatch) context.Context

NewContext returns a new Context that carries value u.

func StatusError

func StatusError(w http.ResponseWriter, r *http.Request, err error)

StatusError replies to a request with an appropriate status code and HTTP status text.

Types

type ErrorHandler

type ErrorHandler interface {
	ErrorHTTP(w http.ResponseWriter, r *http.Request, err error)
}

ErrorHandler handles errors that arise while handling http requests.

func StatusErrorHandler

func StatusErrorHandler() ErrorHandler

StatusErrorHandler returns a basic error handler that just returns a HTTP status error response. Any errors are logged before writing the response.

type ErrorHandlerFunc

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

The ErrorHandlerFunc type is an adapter to allow functions to be used as HTTP error handlers.

func (ErrorHandlerFunc) ErrorHTTP

func (f ErrorHandlerFunc) ErrorHTTP(w http.ResponseWriter, r *http.Request, err error)

ErrorHTTP calls f(w, err, code).

type Handler

type Handler interface {
	ServeHTTPErr(http.ResponseWriter, *http.Request) error
}

A Handler responds to an HTTP request. Handler is like http.Handler but may return an error.

func FallibleFunc

func FallibleFunc(h http.Handler) Handler

FallibleFunc adapts an infallible http handler with no return value to return an error. The returned error is always nil.

type HandlerFunc

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

The HandlerFunc type is an adapter to allow the use of ordinary functions as HTTP handlers. If f is a function with the appropriate signature, HandlerFunc(f) is a Handler that calls f.

func (HandlerFunc) ServeHTTPErr

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

ServeHTTPErr calls f(w, r).

type MethodSet

type MethodSet []string

MethodSet is a set of HTTP methods.

func AnyMethod

func AnyMethod() MethodSet

AnyMethod returns a new MethodSet of all of the commonly known HTTP methods. The set of all methods is not known, thus this uses the more common interpretation of any method defined in RFC 7231 section 4.3 & RFC 5789.

func Methods

func Methods(methods ...string) MethodSet

Methods combines the given HTTP methods into a MethodSet. Duplicates are exluded to preserve set semantics.

func (MethodSet) Add

func (m MethodSet) Add(method string) MethodSet

Add adds method to m and returns a new MethodSet.

func (MethodSet) Has

func (m MethodSet) Has(method string) bool

Has returns true if m contains method.

func (MethodSet) String

func (m MethodSet) String() string

String implements fmt.Stringer.

type MuxMatch

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

MuxMatch represents a matched handler for a given request. The MuxMatch provides access to the pattern that matched and the values extracted from the path for any dynamic parameters that appear in the pattern.

func FromContext

func FromContext(ctx context.Context) (*MuxMatch, bool)

FromContext returns the MuxMatch value stored in ctx, if any.

func (*MuxMatch) Handler

func (m *MuxMatch) Handler(method string) Handler

Handler returns the handler registered for method. Handler returns nil if a handler is not registered for method.

func (*MuxMatch) Methods

func (m *MuxMatch) Methods() MethodSet

Methods returns all of the methods this MuxMatch responds to.

func (*MuxMatch) Param

func (m *MuxMatch) Param(name string) string

Param returns the parameter value for the given placeholder name.

func (*MuxMatch) Params

func (m *MuxMatch) Params() []string

Params returns the matched parameters from the URL in the order that they appear in the pattern.

func (*MuxMatch) Pattern

func (m *MuxMatch) Pattern() string

Pattern returns the URL pattern for the match.

func (*MuxMatch) Reset

func (m *MuxMatch) Reset()

Reset clears the MuxMatch for re-use.

type ServeMux

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

ServeMux is an HTTP request multiplexer. It matches the method and URL of each incoming request against a list of registered routes and calls the handler for the method and pattern that most closely matches the request.

Patterns name paths like "/users". A pattern may contain dynamic path segments. The syntax for patterns is a subset of the browser's URL Pattern API:

  • Literal strings which will be matched exactly.
  • Wildcards of the form "/users/*" match any string.
  • Named groups of the form "/users/:id" match any string like wildcards, but assign a name that can be used to lookup the matched segment.

Placeholders may only appear between slashes, as in "/users/:id/profile", or as the last path segment, as in "/images/*".

Requests are matched by first looking for an exact match, then falling back to pattern matches. Thus the pattern "/users/new" would win over "/users/:id". The weight of named and un-named parameters is the same.

More specific matches are prioritized over less specific matches. For example, if both "/users" and "/users/:id" are registered, a request for "/users/1" would match "/users/:id".

If multiple routes are registered for the same method and pattern, even if the parameter names are different, ServeMux will panic.

func New

func New() *ServeMux

New allocates and returns a new ServeMux ready for use.

func (*ServeMux) Handle

func (mux *ServeMux) Handle(method, pattern string, handler Handler)

Handle registers the handler for the given method and pattern. If a handler already exists for method and pattern, Handle panics.

func (*ServeMux) HandleError

func (mux *ServeMux) HandleError(errHandler ErrorHandler)

HandleError registers the error handler for mux.

func (*ServeMux) HandleErrorFunc

func (mux *ServeMux) HandleErrorFunc(errHandler ErrorHandlerFunc)

HandleErrorFunc registers the error handler function for mux.

func (*ServeMux) HandleFunc

func (mux *ServeMux) HandleFunc(method, pattern string, handler func(http.ResponseWriter, *http.Request) error)

HandleFunc registers the handler function for the given method and pattern.

func (*ServeMux) HandleMethods

func (mux *ServeMux) HandleMethods(methods MethodSet, pattern string, handler Handler)

Handle registers the handler for the given methods and pattern.

func (*ServeMux) HandleMethodsFunc

func (mux *ServeMux) HandleMethodsFunc(methods MethodSet, pattern string, handler func(http.ResponseWriter, *http.Request) error)

HandleMethodsFunc registers the handler function for the given methods and pattern.

func (*ServeMux) Lookup

func (mux *ServeMux) Lookup(r *http.Request) *MuxMatch

Lookup finds the handlers matching the URL of r.

func (*ServeMux) ServeHTTP

func (mux *ServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHttp implements http.Handler by dispatching the request to the handler whose method and pattern most closely matches the request URL.

func (*ServeMux) ServeHTTPErr

func (mux *ServeMux) ServeHTTPErr(w http.ResponseWriter, r *http.Request) error

ServeHTTPErr dispatches the request to the handler whose method and pattern most closely matches the request URL, forwarding any errors.

Directories

Path Synopsis
_examples

Jump to

Keyboard shortcuts

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