httpdispatch

package module
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Nov 25, 2019 License: MIT Imports: 6 Imported by: 1

README

httpdispatch

CircleCI Coverage GoDoc

httpdispatch is a lightweight high performance HTTP request dispatcher (also called multiplexer or mux for short) for Golang, which heavily inspired from httprouter.

In contrast to the [default mux][http.ServeMux] of Go's net/http package, the dispatcher supports variables in the routing pattern and matches against the request method. It also scales more.

The dispatcher is optimized for high performance and a small memory footprint. It scales well even with very long paths and a large number of routes. A compressing dynamic trie (radix tree) structure is used for efficient matching.

Features

Only explicit matches: With other routers, like [http.ServeMux][http.ServeMux], a requested URL path could match multiple patterns. Therefore they have some awkward pattern priority rules, like longest match or first registered, first matched. By design of this router, a request can only match exactly one or no route. As a result, there are also no unintended matches, which makes it great for SEO and improves the user experience.

Stop caring about trailing slashes: Choose the URL style you like, the router automatically redirects the client if a trailing slash is missing or if there is one extra. Of course it only does so, if the new path has a handler. If you don't like it, you can turn off RedirectTrailingSlash and it will return registed handler straightly.

Path auto-correction: Besides detecting the missing or additional trailing slash at no extra cost, the router can also fix wrong cases and remove superfluous path elements (like ../ or //). Is CAPTAIN CAPS LOCK one of your users? httpdispatch can help him by making a case-insensitive look-up and redirecting him to the correct URL.

Parameters in your routing pattern: Stop parsing the requested URL path, just give the path segment a name and the router delivers the dynamic value to you. Because of the design of the router, path parameters are very cheap.

Zero Garbage: The matching and dispatching process generates zero bytes of garbage. In fact, the only heap allocations that are made, is by building the slice of the key-value pairs for path parameters. If the request path contains no parameters, not a single heap allocation is necessary.

No more server crashes: You can set a [Panic Handler][Dispatcher.PanicHandler] to deal with panics occurring during handling a HTTP request. The router then recovers and lets the PanicHandler log what happened and deliver an error response for human.

Perfect for APIs: The router design encourages to build sensible, hierarchical RESTful APIs. Moreover it has builtin native support for OPTIONS Requests and 405 Method Not Allowed replies.

Of course you can also set custom [NotFound][Dispatcher.NotFound] and [MethodNotAllowed][Dispatcher.MethodNotAllowed] handlers and [serve static files][Dispatcher.ServeFiles].

Usage

This is just a quick introduction, view the GoDoc for details.

Let's start with a trivial example:

package main

import (
	"fmt"
	"log"
	"net/http"

	"github.com/dolab/httpdispatch"
)

func Index(w http.ResponseWriter, r *http.Request) {
	fmt.Fprint(w, "Welcome!\n")
}

func Hello(w http.ResponseWriter, r *http.Request) {
	params := httpdispatch.ContextParams(r)

	fmt.Fprintf(w, "Hello, %s!\n", params.ByName("name"))
}

// for high performance
type PingPong struct{}

func (pp PingPong) Handle(w http.ResponseWriter, r *http.Request, params httpdispatch.Params) {
	fmt.Fprintf(w, "Pong, %s!\n", params.ByName("pong"))
}

func main() {
	router := httpdispatch.New()
	router.GET("/", http.HandlerFunc(Index))
	router.GET("/hello/:name", http.HandlerFunc(Hello))
	router.Handle(http.MethodGet, "/ping/:pong", PingPong{})

	log.Fatal(http.ListenAndServe(":8080", router))
}
URL named parameters

As you can see, :name is a named parameter. The values are accessible via httpdispatch.Params, which is just a slice of httpdispatch.Params. You can get the value of a parameter either by its index in the slice, or by using the ByName(name) method: :name can be retrived by ByName("name").

NOTE: URL named parameters only match a single path segment!

Pattern: /user/:user

 /user/gordon              match
 /user/you                 match
 /user/gordon/profile      no match
 /user/                    no match

Note: Since this router has only explicit matches, you can not register static routes and parameters for the same path segment. For example you can not register the patterns /user/new and /user/:user for the same request method at the same time. The routing of different request methods is independent from each other.

URL wildcard parameters

The second type are wildcard (catch-all) parameters and have the form *name. Like the name suggests, they match everything. Therefore they must always be at the end of the pattern:

Pattern: /static/*filepath

 /static/                     match
 /static/somefile.go          match
 /static/subdir/somefile.go   match

How does it work?

The router relies on a tree structure which makes heavy use of common prefixes, it is basically a compact prefix tree (or just Radix tree). Nodes with a common prefix also share a common parent. Here is a short example what the routing tree for the GET request method could look like:

Priority   Path             Handle
9          \                *<1>
3          ├s               nil
2          |├earch\         *<2>
1          |└upport\        *<3>
2          ├blog\           *<4>
1          |    └:post      nil
1          |         └\     *<5>
2          ├about-us\       *<6>
1          |        └team\  *<7>
1          └contact\        *<8>

Every *<num> represents the memory address of a handler function (a pointer). If you follow a path through the tree from the root to the leaf, you get the complete route path, e.g \blog\:post\, where :post is just a placeholder (parameter) for an actual post name. Unlike hash-maps, a tree structure also allows us to use dynamic parts like the :post parameter, since we actually match against the routing patterns instead of just comparing hashes. This works very well and efficient.

Since URL paths have a hierarchical structure and make use only of a limited set of characters (byte values), it is very likely that there are a lot of common prefixes. This allows us to easily reduce the routing into ever smaller problems. Moreover the router manages a separate tree for every request method. For one thing it is more space efficient than holding a method->handle map in every single node, for another thing is also allows us to greatly reduce the routing problem before even starting the look-up in the prefix-tree.

For even better scalability, the child nodes on each tree level are ordered by priority, where the priority is just the number of handles registered in sub nodes (children, grandchildren, and so on..). This helps in two ways:

  1. Nodes which are part of the most routing paths are evaluated first. This helps to make as much routes as possible to be reachable as fast as possible.

  2. It is some sort of cost compensation. The longest reachable path (highest cost) can always be evaluated first. The following scheme visualizes the tree structure. Nodes are evaluated from top to bottom and from left to right.

Does this work with http.Handler?

It does! The router itself implements the http.Handler interface. Moreover the router provides convenient [adapters for http.Handler][Dispatcher.Handler]s and [http.HandlerFunc][Dispatcher.HandlerFunc]s which allows them to be used as a [httpdispatch.Handle][Dispatcher.Handle] when registering a route. The only different is, that parameter values should be retrieved using [httpdispatch.ContextParams] when a http.Handler or http.HandlerFunc is used. Therefore [httpdispatch.Handle][Dispatcher.Handle] has a third func parameter for easy access.

Just try it out for yourself, the usage of httpdispatch is very straightforward. The package is compact and minimalistic, but also probably one of the easiest routers to set up.

Where can I find Middleware X?

This package just provides a very efficient request dispatcher with a few extra features. The router is just a [http.Handler][http.Handler], you can chain any http.Handler compatible middleware before the router, for example the Gorilla handlers. Or you could just write your own, it's very easy!

Alternatively, you could try a web framework build on httpdispatch.

Multi-domain / Sub-domains

Here is a quick example: Does your server serve multiple domains / hosts? You want to use sub-domains? Define a router per host!

package main

import (
	"fmt"
	"log"
	"net/http"

	"github.com/dolab/httpdispatch"
)

// We need an object that implements the http.Handler interface.
// Therefore we need a type for which we implement the ServeHTTP method.
// We just use a map here, in which we map host names (with port) to http.Handlers
type DomainHandlers map[string]http.Handler

// Implement the ServerHTTP method on our new type
func (dh DomainHandlers) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// Check if a http.Handler is registered for the given host.
	// If yes, use it to handle the request.
	if handler := dh[r.Host]; handler != nil {
		handler.ServeHTTP(w, r)
	} else {
		// Handle host names for wich no handler is registered
		http.Error(w, "Not Found", http.StatusNotFound)
	}
}

func Index(w http.ResponseWriter, r *http.Request) {
	fmt.Fprint(w, "Index!\n")
}

func Hello(w http.ResponseWriter, r *http.Request) {
	params := httpdispatch.ContextParams(r)

	fmt.Fprint(w, "Hello, "+params.ByName("name")+"!\n")
}

func main() {
	// Initialize a dispatcher as usual
	router := httpdispatch.New()
	router.GET("/", http.HandlerFunc(Index))
	router.GET("/hello/:name", http.HandlerFunc(Hello))

	// Make a new DomainHandlers and insert the dispatcher (our http handler)
	// for example.com and port 8080
	dh := make(DomainHandlers)
	dh["example.com:8080"] = router

	// Use the DomainHandlers to listen and serve on port 8080
	log.Fatal(http.ListenAndServe(":8080", dh))
}
Basic Authentication

Another quick example: Basic Authentication (RFC 2617) for handles:

package main

import (
	"bytes"
	"encoding/base64"
	"fmt"
	"log"
	"net/http"
	"strings"

	"github.com/dolab/httpdispatch"
)

func BasicAuth(h httpdispatch.Handle, user, pass []byte) httpdispatch.Handle {
	return httpdispatch.NewContextHandle(
		http.HandlerFunc(
			func(w http.ResponseWriter, r *http.Request, ps httpdispatch.Params) {
				const basicAuthPrefix string = "Basic "

				// Get the Basic Authentication credentials
				auth := r.Header.Get("Authorization")
				if strings.HasPrefix(auth, basicAuthPrefix) {
					// Check credentials
					payload, err := base64.StdEncoding.DecodeString(auth[len(basicAuthPrefix):])
					if err == nil {
						pair := bytes.SplitN(payload, []byte(":"), 2)
						if len(pair) == 2 &&
							bytes.Equal(pair[0], user) &&
							bytes.Equal(pair[1], pass) {

							// Delegate request to the given handle
							h(w, r, ps)
							return
						}
					}
				}

				// Request Basic Authentication otherwise
				w.Header().Set("WWW-Authenticate", "Basic realm=Restricted")
				http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
			}
		)
	)
}

func Index(w http.ResponseWriter, r *http.Request) {
	fmt.Fprint(w, "Not protected!\n")
}

func Protected(w http.ResponseWriter, r *http.Request, _ httpdispatch.Params) {
	fmt.Fprint(w, "Protected!\n")
}

func main() {
	user := []byte("gordon")
	pass := []byte("secret!")

	router := httpdispatch.New()
	router.GET("/", http.HandlerFunc(Index))
	router.Handle(http.MethodGet, "/protected/", BasicAuth(Protected, user, pass))

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

Chaining with the NotFound handler

NOTE: It might be required to set [Dispatcher.HandleMethodNotAllowed][Dispatcher.HandleMethodNotAllowed] to false to avoid problems.

You can use another [http.Handler][http.Handler], for example another router, to handle requests which could not be dispatched by this router by using the [Dispatcher.NotFound][Dispatcher.NotFound] handler. This allows chaining.

Static files

The NotFound handler can for example be used to serve static files from the root path / (like an index.html file along with other assets):

// Serve static files from the ./public directory
router.NotFound = http.FileServer(http.Dir("public"))

But this approach sidesteps the strict core rules of this router to avoid routing problems. A cleaner approach is to use a distinct sub-path for serving files, like /static/*filepath or /files/*filepath.

Web Frameworks build on httpdispatch

If the httpdispatch is a bit too minimalistic for you, you might try one of the following more high-level 3rd-party web frameworks building upon the httpdispatch package:

  • gogo: Modern Golang Web Framework

[http.Handler]: <https://golang.org/pkg/net/http/#Handler [http.ServeMux]: https://golang.org/pkg/net/http/#ServeMux [Dispatcher.Handle]: https://godoc.org/github.com/dolab/httpdispatch#Dispatcher.Handle [Dispatcher.Handler]: https://godoc.org/github.com/dolab/httpdispatch#Dispatcher.Handler [Dispatcher.HandlerFunc]: https://godoc.org/github.com/dolab/httpdispatch#Dispatcher.HandlerFunc [Dispatcher.HandleMethodNotAllowed]: https://godoc.org/github.com/dolab/httpdispatch#Dispatcher.HandleMethodNotAllowed [Dispatcher.NotFound]: https://godoc.org/github.com/dolab/httpdispatch#Dispatcher.NotFound [Dispatcher.MethodNotAllowed]: https://godoc.org/github.com/dolab/httpdispatch#Dispatcher.MethodNotAllowed [Dispatcher.PanicHandler]: https://godoc.org/github.com/dolab/httpdispatch#Dispatcher.PanicHandler [Dispatcher.ServeFiles]: https://godoc.org/github.com/dolab/httpdispatch#Dispatcher.ServeFiles

Documentation

Overview

Package httpdispatch is a trie based high performance HTTP request dispatcher.

A trivial example is:

 package main

 import (
     "fmt"
     "net/http"
     "log"

     "github.com/dolab/httpdispatch"
 )

 func Index(w http.ResponseWriter, r *http.Request) {
     fmt.Fprint(w, "Welcome!\n")
 }

 func Hello(w http.ResponseWriter, r *http.Request) {
		params := httpdispatch.ContextParams(r)

     fmt.Fprintf(w, "hello, %s!\n", params.ByName("name"))
 }

 func main() {
     router := httpdispatch.New()
     router.GET("/", http.HandlerFunc(Index))
     router.GET("/hello/:name", http.HandlerFunc(Hello))

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

The router matches incoming requests by the request method and the path. If a handler is registered for this path and method, the router delegates the request to that function. For the methods OPTIONS, GET, POST, PUT, PATCH and DELETE shortcut functions exist to register handlers, for all other methods *dispatcher.Handler can be used.

The registered path, against which the router matches incoming requests, can contain two types of parameters:

Syntax    Type
:name     named parameter
*name     wildcard parameter

Named parameters are dynamic path segments. They match anything until the next '/' or the path end:

Path: /blog/:category/:post

Requests:
 /blog/go/request-routers            match: category="go", post="request-routers"
 /blog/go/request-routers/           no match, but the router would redirect to /blog/go/request-routers
 /blog/go/                           no match
 /blog/go/request-routers/comments   no match

Wildcard parameters match anything until the path end, including the directory index (the '/' before the wildcard). Since they match anything until the end, wildcard parameters must always be the final path element.

Path: /files/*filepath

Requests:
 /files/                             match: filepath="/"
 /files/LICENSE                      match: filepath="/LICENSE"
 /files/templates/article.html       match: filepath="/templates/article.html"
 /files                              no match, but the router would redirect

The value of parameters is saved as a slice of the Param struct, consisting each of a key and a value. The slice is passed to the Handle func as a third parameter. There are two ways to retrieve the value of a parameter:

// by the name of the parameter
user := ps.ByName("user") // defined by :user or *user

// by the index of the parameter. This way you can also get the name (key)
thirdKey   := ps[2].Key   // the name of the 3rd parameter
thirdValue := ps[2].Value // the value of the 3rd parameter

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Normalize

func Normalize(p string) string

Normalize is the URL version of path.Clean, it returns a canonical URL path for p, eliminating . and .. elements.

The following rules are applied iteratively until no further processing can be done:

  1. Replace multiple slashes with a single slash.
  2. Eliminate each . path name element (the current directory).
  3. Eliminate each inner .. path name element (the parent directory) along with the non-.. element that precedes it.
  4. Eliminate .. elements that begin a rooted path: that is, replace "/.." by "/" at the beginning of a path.

If the result of this process is an empty string, "/" is returned

Types

type ContextHandle

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

ContextHandle defines container of registered http.Handler with useful context, such as package name, controller name and action name of handle.

func NewContextHandle

func NewContextHandle(handler http.Handler, useContext bool) *ContextHandle

NewContextHandle returns *ContextHandle with handler info

func (*ContextHandle) Handle

func (ch *ContextHandle) Handle(w http.ResponseWriter, r *http.Request, ps Params)

Handle hijacks http.Handler with request params

type Dispatcher

type Dispatcher struct {

	// If enabled, the router tries to inject parsed params within http.Request.
	RequestContext bool

	// Enables automatic redirection if the current route can't be matched but a
	// handler for the path with (without) the trailing slash exists.
	// For example if /foo/ is requested but a route only exists for /foo, the
	// client is redirected to /foo with http status code 301 for GET requests
	// and 307 for all other request methods.
	RedirectTrailingSlash bool

	// If enabled, the router tries to fix the current request path, if no
	// handle is registered for it.
	// First superfluous path elements like ../ or // are removed.
	// Afterwards the router does a case-insensitive lookup of the cleaned path.
	// If a handle can be found for this route, the router makes a redirection
	// to the corrected path with status code 301 for GET requests and 307 for
	// all other request methods.
	// For example /FOO and /..//Foo could be redirected to /foo.
	// RedirectTrailingSlash is independent of this option.
	RedirectFixedPath bool

	// If enabled, the router checks if another method is allowed for the
	// current route, if the current request can not be routed.
	// If this is the case, the request is answered with 'Method Not Allowed'
	// and HTTP status code 405.
	// If no other Method is allowed, the request is delegated to the NotFound
	// handler.
	HandleMethodNotAllowed bool

	// If enabled, the router automatically replies to OPTIONS requests.
	// Custom OPTIONS handlers take priority over automatic replies.
	HandleMethodOPTIONS bool

	// Configurable http.Handler which is called when no matching route is
	// found. If it is not set, http.NotFound is used.
	NotFound http.Handler

	// Configurable http.Handler which is called when a request
	// cannot be routed and HandleMethodNotAllowed is true.
	// If it is not set, http.Error with http.StatusMethodNotAllowed is used.
	// The "Allow" header with allowed request methods is set before the handler
	// is called.
	MethodNotAllowed http.Handler

	// Configurable http.Handler which is called for OPTIONS request.
	// The "Allow" header with allowed request methods is set before the handler
	// is called.
	MethodOptions http.Handler

	// Function to handle panics recovered from http handlers.
	// It should be used to generate an error page and return the http error code
	// 500 (Internal Server Error).
	// The handler can be used to keep your server from crashing because of
	// unrecovered panics.
	PanicHandler func(http.ResponseWriter, *http.Request, interface{})
	// contains filtered or unexported fields
}

Dispatcher is a http.Handler which can be used to dispatch requests to different handler functions via configurable routes

func New

func New() *Dispatcher

New returns a new initialized Dispatcher. Path auto-correction, including trailing slashes, is enabled by default.

func (*Dispatcher) DELETE

func (dp *Dispatcher) DELETE(uripath string, handler http.Handler)

DELETE is a shortcut for dispatcher.Handler("GET", path, http.Handler)

func (*Dispatcher) GET

func (dp *Dispatcher) GET(uripath string, handler http.Handler)

GET is a shortcut for dispatcher.Handler("GET", path, http.Handler)

func (*Dispatcher) HEAD

func (dp *Dispatcher) HEAD(uripath string, handler http.Handler)

HEAD is a shortcut for dispatcher.Handler("GET", path, http.Handler)

func (*Dispatcher) Handle

func (dp *Dispatcher) Handle(method, uripath string, handler Handler)

Handle registers a new request handler with the given path and method. This is e.g. useful to build a framework around the dispatcher.

For OPTIONS, GET, POST, PUT, PATCH and DELETE requests the respective shortcut funcs can be used.

This func is intended for bulk loading and to allow the usage of less frequently used, non-standardized or custom methods (e.g. for internal communication with a proxy).

func (*Dispatcher) Handler

func (dp *Dispatcher) Handler(method, uripath string, handler http.Handler)

Handler is an adapter which allows the usage of a http.Handler as a request handle.

func (*Dispatcher) HandlerFunc

func (dp *Dispatcher) HandlerFunc(method, uripath string, handler http.HandlerFunc)

HandlerFunc is an adapter which allows the usage of an http.HandlerFunc as a request handler.

func (*Dispatcher) Lookup

func (dp *Dispatcher) Lookup(method, uripath string) (Handler, Params, bool)

Lookup allows the manual lookup of a method + path combo. This is e.g. useful to build a framework around the dispatcher. If the path was found, it returns the handler func and the captured parameter values.

NOTE: It returns handler when the third returned value indicates a redirection to the same path with / without the trailing slash should be performed.

func (*Dispatcher) OPTIONS

func (dp *Dispatcher) OPTIONS(uripath string, handler http.Handler)

OPTIONS is a shortcut for dispatcher.Handler("GET", path, http.Handler)

func (*Dispatcher) PATCH

func (dp *Dispatcher) PATCH(uripath string, handler http.Handler)

PATCH is a shortcut for dispatcher.Handler("GET", path, http.Handler)

func (*Dispatcher) POST

func (dp *Dispatcher) POST(uripath string, handler http.Handler)

POST is a shortcut for dispatcher.Handler("GET", path, http.Handler)

func (*Dispatcher) PUT

func (dp *Dispatcher) PUT(uripath string, handler http.Handler)

PUT is a shortcut for dispatcher.Handler("GET", path, http.Handler)

func (*Dispatcher) ServeFiles

func (dp *Dispatcher) ServeFiles(filename string, fs http.FileSystem)

ServeFiles serves files from the given file system root. The path must end with "/*filepath", files are then served from the local path /path/to/*filepath. For example if root is "/etc" and *filepath is "passwd", the local file "/etc/passwd" would be served. Internally a http.FileServer is used, therefore http.NotFound is used instead of the Dispatcher's NotFound handler. To use the operating system's file system implementation, use http.Dir:

router.ServeFiles("/static/*filepath", http.Dir("/var/www"))

func (*Dispatcher) ServeHTTP

func (dp *Dispatcher) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP makes the router implement the http.Handler interface.

type FileHandle

type FileHandle struct {
	*ContextHandle
}

FileHandle defines static files server context

func NewFileHandle

func NewFileHandle(fs http.FileSystem) *FileHandle

NewFileHandle returns *FileHandle with passed http.HandlerFunc

func (*FileHandle) Handle

func (fh *FileHandle) Handle(w http.ResponseWriter, r *http.Request, ps Params)

Handle hijacks request path with filepath by overwrite

type Handler

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

Handler is an interface that can be registered to a route to handle HTTP requests. Like http.HandlerFunc, but has a third parameter for the values of wildcards (variables).

type Param

type Param struct {
	Key   string
	Value string
}

Param is a single URL parameter, consisting of a key and a value.

type Params

type Params []Param

Params is a Param-slice, as returned by the dispatcher. The slice is ordered, the first URL parameter is also the first slice value. It is therefore safe to read values by the index.

func ContextParams

func ContextParams(r *http.Request) Params

ContextParams pulls the URL parameters from a request context, or returns nil if none are present.

This is only present from go 1.7.

func (Params) ByName

func (ps Params) ByName(name string) string

ByName returns the value of the first Param which key matches the given name. If no matching Param is found, an empty string is returned.

func (Params) DefName

func (ps Params) DefName(name string) (string, bool)

DefName returns the value and true of the first Param which key matches the given name. If no matching Param is found, an empty string and false is returned.

Jump to

Keyboard shortcuts

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