router

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

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

Go to latest
Published: Mar 6, 2018 License: MIT Imports: 3 Imported by: 1

README

Router

NOTICE: This project is no longer actively maintained. Please take a look at lufia's fork which contains some fixes.

Package router provides a simple yet powerful URL router and HandlerFunc dispatcher for web apps.

Ideas considered (heavily borrowing from express/connect):

  • registering handlers based on HTTP verb/path combos should be easy, as this is most often used
  • split complex HandlerFuncs into multiple smaller one which can be shared
  • mount generic HandlerFuncs to be executed on every path
  • registering and accessing paths with params (like :userid) should be easy
  • store data on a requestContext, so it can be passed to later HandlerFuncs
  • set a generic errorHandlerFunc and stop executing later handerFuncs as soon as an error occurs
  • set a generic pageNotFound HandlerFunc
  • handlers are regular http.HandlerFunc to be compatible with go

Build Status GoDoc

Quickstart

After installing Go and setting up your GOPATH, create a server.go file.

package main

import (
	"github.com/toonketels/router"
	"net/http"
)

func main() {
	// Create a new router
	appRouter := router.NewRouter()

	// Register a handlerFunc for GET/"hello" paths
	appRouter.Get("/hello", func(res http.ResponseWriter, req *http.Request) {
		res.Write([]byte("hello"))
	})

	// Use this router
	appRouter.Handle("/")

	// Listen for requests
	http.ListenAndServe(":3000", nil)
}

Then install the router package:

go get github.com/toonketels/router

Then run your server:

go run server.go

Next

Take a look at the examples in the repo or run and inspect the tests.

The guide below goes a bit deeper into the api. And don't forget godoc for api documentation.

Guide

Table of contents
HandlerFuncs

Once a router instance has been created, use it's Get/Put/Post/Patch/Head/Options/Delete methods to register a handlerFunc to a route/HTTP verb pair.

// Register a handlerFunc for GET/"hello" paths
appRouter.Get("/hello", handlerFunc)

// Register a handlerFunc for PUT/"hello" paths
appRouter.Put("/hello", handlerFunc)

// Register a handlerFunc for POST/"hello" paths
appRouter.Post("/hello", handlerFunc)

// Register a handlerFunc for Patch/"hello" paths
appRouter.Patch("/hello", handlerFunc)

// Register a handlerFunc for HEAD/"hello" paths
appRouter.Head("/hello", handlerFunc)

// Register a handlerFunc for OPTIONS/"hello" paths
appRouter.Options("/hello", handlerFunc)

// Register a handlerFunc for DELETE/"hello" paths
appRouter.Delete("/hello", handlerFunc)

You can also register multiple handlerFuncs for a given route.

// Register the handlerFuncs
appRouter.Get("/user/:userid/hello", logUser, handleUser)

For this to work, all handlerFuncs should pass control to handlerFuncs coming after them by calling cntxt.Next().

func loadUser(res http.ResponseWriter, req *http.Request) {
	// Grab the context for the current request
	cntxt := router.Context(req)
	// Do something

	// Pass over control to next handlerFunc
	cntxt.Next(res, req)
}

If your handlerFunc wants to protect access to certain routes, it could do so by only calling cntxt.Next() when some authorization rule validates.

func allowAccess(res http.ResponseWriter, req *http.Request) {
	if allowUserAccess() {
		// Allows access
		router.Context(req).Next(res, req)
		return
	}
	// Denies access
}

HandlerFuncs can store data onto the requestContext to be used by handlerFuncs after them.

func loadUser(res http.ResponseWriter, req *http.Request) {
	cntxt := router.Context(req)
	user := getUserFromDB(cntxt.Params["userid"])

	// Store the value in request specific store
	_ = cntxt.Set("user", user)

	cntxt.Next(res, req)
}

HandlerFuncs use cntxt.Get(key) to get the value. RequestContext has Set/ForceSet/Get/Delete methods all related to the data stored during the current request.

func handleUser(res http.ResponseWriter, req *http.Request) {
	cntxt := router.Context(req)

	// Get a value from the request specific store
	if user, ok := cntxt.Get("user"); ok {
		// Do something
	}
	// Do something else
}

Remember the route /user/:userid/hello? It matches routes like /user/14/hello or /user/richard/hello. HandlerFuncs can access the values of userid on the requestContext.

func loadUser(res http.ResponseWriter, req *http.Request) {

	// Grab the userid param from the context
	userid := router.Context(req).Params["userid"]

	// Do something with it
}

As you might have noticed, handlers need to be http.HandlerFunc's. So you can use your existing ones if you don't need to access the requestContext.

Mounting handlerFuncs

Some handlerFuncs need to be executed for every request, like a logger. Instead of passing it to every registered route, we "mount" them using router.Mount().

appRouter := router.NewRouter()

// Mount handlerFuncs first
appRouter.Mount("/", logger)
appRouter.Mount("/", allowAccess)

// Then start matching paths
appRouter.Get("/user/:userid/hello", loadUser, handleUser)

The order in which you mounted and registered handlers, is the order in which they will be executed.

A request to /user/14/hello will result in logger to be called first, followed by allowAccess, loadUser and handleUser. That is as long as none of the handlerFunc's prevented the latter ones from executing by not calling next.

By changing the mountPath of allowAccess to /admin, we get different results.

// Mount handlerFuncs first
appRouter.Mount("/", logger)
appRouter.Mount("/admin", allowAccess)

// Then start matching paths
appRouter.Get("/user/:userid/hello", loadUser, handleUser)
appRouter.Get("/admin/user/:userid", loadUser, administerUserHandler)

It the above case a request to /user/20/hello will execute logger -> loadUser -> handleUser, while a request to /admin/user/20 executes logger -> allowAccess -> loadUser -> administerUserHandler.

We see that by dividing a complex handlerFunc into multiple smaller ones, we get more code reuse. It becomes easy to create a small set of "middleware" handlers to be reused on different routes. While the last handlerFunc is generally the one responsible for generating the actual response.

Error Handling

Besides storing data and dispatching the next handlerFunc cntxt has an Error method. Let's update the loadUser handlerFunc to take errors into account.

func loadUser(res http.ResponseWriter, req *http.Request) {
	cntxt := router.Context(req)
	user, err := getUserFromDB(cntxt.Params["userid"])
	if err != nil {

		// Let the errorHandlerFunc generate the error response.
		// We stop executing the following handlers
		cntxt.Error(res, req, err.Error(), 500)
		return
	}

	// Store the value in request specific store
	_ = cntxt.Set("user", user)

	// Pass over control to next handlerFunc
	cntxt.Next(res, req)
}

Calling cntxt.Error() notifies the requestContext an error has been made and further Next() call will be prevented. It delegates the requestHandling to a dedicated errorHandlerFunc to reply in a consistent manner.

Though calling Next() after an error will never dispatch the next HandlerFunc, it is wise to just return after the error so the current HandlerFunc stops executing.

Previous HandlerFuncs are allowed to continue executing their code when the executing flow returns to them.

For instance, when the logger below is called, it records the time and calls the next HandlerFunc. If that handler errs, logger will resume as usual allowing it to log its output.

func logger(res http.ResponseWriter, req *http.Request) {

	// The fist handlerFunc to be executed
	// record the time when the request started
	start := time.Now()

	// Handle over control to the next handlerFunc.
	router.Context(req).Next(res, req)

	// We log once all other handlerFuncs are done executing
	// so it needs to come after our call to cntxt.Next()
	fmt.Println(req.Method, req.URL.Path, time.Since(start))
}

The actual response send to the client is handled by default ErrorRequestHandler. Which will just do http.Error(res, err, code).

Customize the response by updating your router's ErrorHandler. The function passed should comply with the ErrorHandler interface.

appRouter.ErrorHandler = func(res http.ResponseWriter, req *http.Request, err string, code int) {
	http.Error(res, strings.ToUpper(err), code)
}

The request is passed to allow a different response to be send depending on request properties.

Similarly, configure the response generated when a route is not found by updating the router's NotFoundHandler which is a plain http.HandlerFunc.

Documentation

Overview

Package router provides a simple yet powerful URL router and HandlerFunc dispatcher for web apps.

Ideas considered (heavily borrowing from express/connect):

  • registering handlers based on HTTP verb/path combos should be easy, as this is most often used
  • split complex HandlerFuncs into multiple smaller one which can be shared
  • mount generic HandlerFuncs to be executed on every path
  • registering and accessing paths with params (like :userid) should be easy
  • store data on a requestContext, so it can be passed to later HandlerFuncs
  • set a generic errorHandlerFunc and stop executing later handerFuncs as soon as an error occurs
  • set a generic pageNotFound HandlerFunc
  • handlers are regular `http.HandlerFunc` to be compatible with go

Basic usage

// Create a new router
appRouter := router.NewRouter()

// Register a HandlerFunc for GET/"hello" paths
appRouter.Get("/hello", func(res http.ResponseWriter, req *http.Request) {
	res.Write([]byte("hello"))
})

// Use this router
appRouter.Handle("/")

// Listen for requests
http.ListenAndServe(":3000", nil)

More advanced usage

func main() {
	appRouter := router.NewRouter()

	// `Mount` mounts a handler for all paths (starting with `/`)
	// Always mount generic HandlerFuncs first.
	appRouter.Mount("/", logger)

	// We can use multiple handleFuncs evaluated in order.
	// `:userid` specifies the param `userid` so it will match any string.
	appRouter.Get("/user/:userid/hello", loadUser, handleUser)

	appRouter.Handle("/")
	http.ListenAndServe(":3000", nil)
}

func logger(res http.ResponseWriter, req *http.Request) {

	// The fist HandlerFunc to be executed
	// record the time when the request started
	start := time.Now()

	// Grab the current context and call
	// cntxt.Next() to handle over control to the next HandlerFunc.
	// Simply don't call cntxt.Next() if you don't want to call the following
	// HandlerFunc's (for instance, for access control reasons).
	router.Context(req).Next(res, req)

	// We log once all other HandlerFuncs are done executing
	// so it needs to come after our call to cntxt.Next()
	fmt.Println(req.Method, req.URL.Path, time.Since(start))
}

func loadUser(res http.ResponseWriter, req *http.Request) {
	cntxt := router.Context(req)
	user, err := getUserFromDB(cntxt.Params["userid"])
	if err != nil {

		// Let the ErrorHandler generate the error response.
		// We stop executing the following handlers
		cntxt.Error(res, req, err.Error(), 500)
		return
	}

	// Store the value in request specific store
	_ = cntxt.Set("user", user)

	// Pass over control to next HandlerFunc
	cntxt.Next(res, req)
}

func handleUser(res http.ResponseWriter, req *http.Request) {
	cntxt := router.Context(req)

	// Get a value from the request specific store
	if user, ok := cntxt.Get("user"); ok {
		if str, ok := user.(string); ok {

			// As last handlers, we should generate a response
			greeting := "Hello " + str
			res.Write([]byte(greeting))
			return
		}
	}
	res.Write([]byte("Who are you?"))

	// We dont use cntxt.Next() as there are no more
	// HandlerFuncs to call. However, stuff wont explode
	// if you call cntxt.Next()` by mistake.
}

// func getUserFromDB...

This will log for each request. On "/user/:userid/hello" matching paths, it loads a user and saves it to the requestContext store and handleUser generates the response. Note all handlers are regular http.HandlerFunc and use a `Context` to hand over control and data to the next HandlerFunc.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type ErrorHandler

type ErrorHandler func(res http.ResponseWriter, req *http.Request, err string, code int)

ErrorHandler interface to which an errorHandler needs to comply.

Used as a field in the router to override the default RrrorHandler implementation. Its responsibility is to generate the http Response when an error occurs. That is, when requestContext.Error() gets called.

type RequestContext

type RequestContext struct {
	Params map[string]string
	// contains filtered or unexported fields
}

RequestContext contains data related to the current request

func Context

func Context(req *http.Request) *RequestContext

Context returns a pointer to the RequestContext for the current request.

func (*RequestContext) Delete

func (cntxt *RequestContext) Delete(key interface{})

Delete removes a key/value pair from the store.

func (*RequestContext) Error

func (cntxt *RequestContext) Error(res http.ResponseWriter, req *http.Request, err string, code int)

Error allows you to respond with an error message preventing the subsequent handlers from being executed.

Note: in case there exist previous requestHandlers and they have code after their next call, that code will execute. This allows loggers and such to finish what they started (though they can also use a defer for that).

func (*RequestContext) ForceSet

func (cntxt *RequestContext) ForceSet(key, val interface{})

ForceSet saves a value for the current request. Unlike Set, it will happily override existing data.

func (*RequestContext) Get

func (cntxt *RequestContext) Get(key interface{}) (val interface{}, ok bool)

Get fetches data from the store associated with the current request.

func (*RequestContext) Next

func (cntxt *RequestContext) Next(res http.ResponseWriter, req *http.Request)

Next invokes the next HandleFunc in line registered to handle this request.

This is needed when multiple HandleFuncs are registered for a given path and allows the creation and use of `middleware`.

func (*RequestContext) Set

func (cntxt *RequestContext) Set(key, val interface{}) bool

Set saves a value for the current request. The value will not be set if the key already exist.

type Router

type Router struct {
	NotFoundHandler http.HandlerFunc // Specify a custom NotFoundHandler
	ErrorHandler    ErrorHandler     // Specify a custom ErrorHandler
	// contains filtered or unexported fields
}

A Router to register paths and requestHandlers to.

Set a custom NotFoundHandler if you want to override go's default one.

There can be multiple per application, if so, don't forget to pass a different pattern to `router.Handle()`.

func NewRouter

func NewRouter() (router *Router)

NewRouter creates a router and returns a pointer to it so you can start registering routes.

Don't forget to call `router.Handle(pattern)` to actually use the router.

func (*Router) Delete

func (router *Router) Delete(path string, handlers ...http.HandlerFunc)

Delete registers a DELETE path to be handled. Multiple handlers can be passed and will be evaluated in order (after the more generic mounted HandlerFuncs).

func (*Router) Get

func (router *Router) Get(path string, handlers ...http.HandlerFunc)

Get registers a GET path to be handled. Multiple handlers can be passed and will be evaluated in order (after the more generic mounted HandlerFuncs).

func (*Router) Handle

func (router *Router) Handle(pattern string)

Handle registers the router for the given pattern in the DefaultServeMux. The documentation for ServeMux explains how patterns are matched.

This delegates to `http.Handle()` internally.

Most of the times, you just want to do `router.Handle("/")`.

func (*Router) Head

func (router *Router) Head(path string, handlers ...http.HandlerFunc)

Head registers an HEAD path to be handled. Multiple handlers can be passed and will be evaluated in order (after the more generic mounted HandlerFuncs).

func (*Router) Mount

func (router *Router) Mount(mountPath string, handler http.HandlerFunc)

Mount mounts a requestHandler for a given mountPath. The requestHandler will be executed on all paths which start like the mountPath.

For example: mountPath "/" will execute the requestHandler for all requests (each one starts with "/"), contrary to "/api" will only execute the handler on paths starting with "/api" like "/api", "/api/2", "api/users/23"...

This allows for the use of general middleware, unlike more specific middleware handlers which are registered on specific paths.

Use mount if your requestHandlers needs to be invoked on all/most paths so you don't have to register it again and again when registering handlers.

The mountPath don't accept tokens (like :user) but can access the params on the context if the path on which it is fired contains those tokens.

func (*Router) Options

func (router *Router) Options(path string, handlers ...http.HandlerFunc)

Options registers an OPTONS path to be handled. Multiple handlers can be passed and will be evaluated in order (after the more generic mounted HandlerFuncs).

func (*Router) Patch

func (router *Router) Patch(path string, handlers ...http.HandlerFunc)

Patch registers a PATCH path to be handled. Multiple handlers can be passed and will be evaluated in order (after the more generic mounted HandlerFuncs).

func (*Router) Post

func (router *Router) Post(path string, handlers ...http.HandlerFunc)

Post registers a POST path to be handled. Multiple handlers can be passed and will be evaluated in order (after the more generic mounted HandlerFuncs).

func (*Router) Put

func (router *Router) Put(path string, handlers ...http.HandlerFunc)

Put registers a PUT path to be handled. Multiple handlers can be passed and will be evaluated in order (after the more generic mounted HandlerFuncs).

func (*Router) ServeHTTP

func (router *Router) ServeHTTP(res http.ResponseWriter, req *http.Request)

Needed by go to actually start handling the registered routes. You don't need to call this yourself.

Directories

Path Synopsis
Advanced example using router.
Advanced example using router.

Jump to

Keyboard shortcuts

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