README

GoDoc Build Status Coverage Status Go Report Card

violetear

Go HTTP router

http://violetear.org

Design Goals
  • Keep it simple and small, avoiding extra complexity at all cost. KISS
  • Support for static and dynamic routing.
  • Easy middleware compatibility so that it satisfies the http.Handler interface.
  • Common context between middleware.
  • Trace Request-ID per request.
  • HTTP/2 native support Push Example
  • Versioning based on Accept header application/vnd.*

Package GoDoc

How it works

The router is capable off handle any kind or URI, static, dynamic or catchall and based on the HTTP request Method accept or discard the request.

For example, suppose we have an API that exposes a service that allow to ping any IP address.

To handle only "GET" request for any IPv4 addresss:

http://api.violetear.org/command/ping/127.0.0.1
                        \______/\___/\________/
                            |     |      |
                             static      |
                                      dynamic

The router HandlerFunc would be:

router.HandleFunc("/command/ping/:ip", ip_handler, "GET")

For this to work, first the regex matching :ip should be added:

router.AddRegex(":ip", `^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$`)

Now let's say you also want to be available to ping ipv6 or any host:

http://api.violetear.org/command/ping/*
                        \______/\___/\_/
                            |     |   |
                             static   |
                                   catch-all

A catch-all could be used and also a different handler, for example:

router.HandleFunc("/command/ping/*", any_handler, "GET, HEAD")

The * indicates the router to behave like a catch-all therefore it will match anything after the /command/ping/ if no other condition matches before.

Notice also the "GET, HEAD", that indicates that only does HTTP methods will be accepted, and any other will not be allowed, router will return a 405 the one can also be customised.

Usage

Requirementes go >= 1.7 (https://golang.org/pkg/context/ required)

import "github.com/nbari/violetear"

HandleFunc:

 func HandleFunc(path string, handler http.HandlerFunc, http_methods ...string)

Handle (useful for middleware):

 func Handle(path string, handler http.Handler, http_methods ...string)

http_methods is a comma separted list of allowed HTTP methods, example:

router.HandleFunc("/view", handleView, "GET, HEAD")

AddRegex adds a ":named" regular expression to the dynamicRoutes, example:

router.AddRegex(":ip", `^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$`)

Basic example:

package main

import (
    "github.com/nbari/violetear"
    "log"
    "net/http"
)

func catchAll(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("I'm catching all\n"))
}

func handleGET(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("I handle GET requests\n"))
}

func handlePOST(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("I handle POST requests\n"))
}

func handleUUID(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("I handle dynamic requests\n"))
}

func main() {
    router := violetear.New()
    router.LogRequests = true
    router.RequestID = "Request-ID"

    router.AddRegex(":uuid", `[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}`)

    router.HandleFunc("*", catchAll)
    router.HandleFunc("/method", handleGET, "GET")
    router.HandleFunc("/method", handlePOST, "POST")
    router.HandleFunc("/:uuid", handleUUID, "GET,HEAD")

    srv := &http.Server{
        Addr:           ":8080",
        Handler:        router,
        ReadTimeout:    5 * time.Second,
        WriteTimeout:   7 * time.Second,
        MaxHeaderBytes: 1 << 20,
    }
    log.Fatal(srv.ListenAndServe())

}

Running this code will show something like this:

$ go run test.go
2015/10/22 17:14:18 Adding path: * [ALL]
2015/10/22 17:14:18 Adding path: /method [GET]
2015/10/22 17:14:18 Adding path: /method [POST]
2015/10/22 17:14:18 Adding path: /:uuid [GET,HEAD]

Using router.Verbose = false will omit printing the paths.

test.go contains the code show above

Testing using curl or http

Any request 'catch-all':

$ http POST http://localhost:8080/
HTTP/1.1 200 OK
Content-Length: 17
Content-Type: text/plain; charset=utf-8
Date: Thu, 22 Oct 2015 15:18:49 GMT
Request-Id: POST-1445527129854964669-1

I'm catching all

A GET request:

$ http http://localhost:8080/method
HTTP/1.1 200 OK
Content-Length: 22
Content-Type: text/plain; charset=utf-8
Date: Thu, 22 Oct 2015 15:43:25 GMT
Request-Id: GET-1445528605902591921-1

I handle GET requests

A POST request:

$ http POST http://localhost:8080/method
HTTP/1.1 200 OK
Content-Length: 23
Content-Type: text/plain; charset=utf-8
Date: Thu, 22 Oct 2015 15:44:28 GMT
Request-Id: POST-1445528668557478433-2

I handle POST requests

A dynamic request using an UUID as the URL resource:

$ http http://localhost:8080/50244127-45F6-4210-A89D-FFB0DA039425
HTTP/1.1 200 OK
Content-Length: 26
Content-Type: text/plain; charset=utf-8
Date: Thu, 22 Oct 2015 15:45:33 GMT
Request-Id: GET-1445528733916239110-5

I handle dynamic requests

Trying to use POST on the /:uuid resource will cause a Method not Allowed 405 this because only GET and HEAD methods are allowed:

$ http POST http://localhost:8080/50244127-45F6-4210-A89D-FFB0DA039425
HTTP/1.1 405 Method Not Allowed
Content-Length: 19
Content-Type: text/plain; charset=utf-8
Date: Thu, 22 Oct 2015 15:47:19 GMT
Request-Id: POST-1445528839403536403-6
X-Content-Type-Options: nosniff

Method Not Allowed

RequestID

To keep track of the "requests" an existing "request ID" header can be used, if the header name for example is Request-ID therefore to continue using it, the router needs to know the name, example:

router := violetear.New()
router.RequestID = "X-Appengine-Request-Log-Id"

If the proxy is using another name, for example "RID" then use something like:

router := violetear.New()
router.RequestID = "RID"

If router.RequestID is not set, no "request ID" is going to be added to the headers. This can be extended using a middleware same has the logger check the AppEngine example.

NotFoundHandler

For defining a custom http.Handler to handle 404 Not Found example:

...

func my404() http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        http.Error(w, "ne ne ne", 404)
    })
}

func main() {
    router := violetear.New()
    router.NotFoundHandler = my404()
    ...

NotAllowedHandler

For defining a custom http.Handler to handle 405 Method Not Allowed.

PanicHandler

For using a custom http.HandlerFunc to handle panics

Middleware

Violetear uses Alice to handle middleware.

Example:

package main

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

	"github.com/nbari/violetear"
	"github.com/nbari/violetear/middleware"
)

func commonHeaders(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("X-app-Version", "1.0")
		next.ServeHTTP(w, r)
	})
}

func middlewareOne(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		log.Println("Executing middlewareOne")
		ctx := context.WithValue(r.Context(), "m1", "m1")
		ctx = context.WithValue(ctx, "key", 1)
		next.ServeHTTP(w, r.WithContext(ctx))
		log.Println("Executing middlewareOne again")
	})
}

func middlewareTwo(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		log.Println("Executing middlewareTwo")
		if r.URL.Path != "/" {
			return
		}
		ctx := context.WithValue(r.Context(), "m2", "m2")
		next.ServeHTTP(w, r.WithContext(ctx))
		log.Println("Executing middlewareTwo again")
	})
}

func catchAll(w http.ResponseWriter, r *http.Request) {
	log.Printf("Executing finalHandler\nm1:%s\nkey:%d\nm2:%s\n",
		r.Context().Value("m1"),
		r.Context().Value("key"),
		r.Context().Value("m2"),
	)
	w.Write([]byte("I catch all"))
}

func foo(w http.ResponseWriter, r *http.Request) {
	panic("this will never happen, because of the return")
}

func main() {
	router := violetear.New()

	stdChain := middleware.New(commonHeaders, middlewareOne, middlewareTwo)

	router.Handle("/", stdChain.ThenFunc(catchAll), "GET,HEAD")
	router.Handle("/foo", stdChain.ThenFunc(foo), "GET,HEAD")
	router.HandleFunc("/bar", foo)

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

Notice the use or router.Handle and router.HandleFunc when using middleware you normally would use route.Handle

Request output example:

$ http http://localhost:8080/
HTTP/1.1 200 OK
Content-Length: 11
Content-Type: text/plain; charset=utf-8
Date: Thu, 22 Oct 2015 16:08:18 GMT
Request-Id: GET-1445530098002701428-3
X-App-Version: 1.0

I catch all

On the server you will see something like this:

$ go run test.go
2016/08/17 18:08:42 Adding path: / [GET,HEAD]
2016/08/17 18:08:42 Adding path: /foo [GET,HEAD]
2016/08/17 18:08:42 Adding path: /bar [ALL]
2016/08/17 18:08:47 Executing middlewareOne
2016/08/17 18:08:47 Executing middlewareTwo
2016/08/17 18:08:47 Executing finalHandler
m1:m1
key:1
m2:m2
2016/08/17 18:08:47 Executing middlewareTwo again
2016/08/17 18:08:47 Executing middlewareOne again

AppEngine

The app.yaml file:

application: 'app-name'
version: 1
runtime: go
api_version: go1

handlers:

- url: /.*
  script: _go_app

The app.go file:

package app

import (
    "appengine"
    "github.com/nbari/violetear"
    "github.com/nbari/violetear/middleware"
    "net/http"
)

func init() {
    router := violetear.New()
    stdChain := middleware.New(requestID)
    router.Handle("*", stdChain.ThenFunc(index), "GET, HEAD")
    http.Handle("/", router)
}

func requestID(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        c := appengine.NewContext(r)
        w.Header().Set("Request-ID", appengine.RequestID(c))
        next.ServeHTTP(w, r)
    })
}

func index(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Hello world!"))
}

Demo: http://api.violetear.org

Using curl or http:

$ http http://api.violetear.org
HTTP/1.1 200 OK
Cache-Control: private
Content-Encoding: gzip
Content-Length: 32
Content-Type: text/html; charset=utf-8
Date: Sun, 25 Oct 2015 06:14:55 GMT
Request-Id: 562c735f00ff0902f823e514a90001657e76696f6c65746561722d31313037000131000100
Server: Google Frontend

Hello world!

Context & Named parameters

In some cases there is a need to pass data across handlers/middlewares, for doing this Violetear uses net/context.

When using dynamic routes :regex, you can use GetParam or GetParams, see below.

Example:

package main

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

    "github.com/nbari/violetear"
)

func catchAll(w http.ResponseWriter, r *http.Request) {
    // Get & print the content of named-param *
    params := r.Context().Value(violetear.ParamsKey).(violetear.Params)
    fmt.Fprintf(w, "CatchAll value:, %q", params["*"])
}

func handleUUID(w http.ResponseWriter, r *http.Request) {
    // get router params
    params := r.Context().Value(violetear.ParamsKey).(violetear.Params)
    // using GetParam
    uuid := violetear.GetParam("uuid", r)
    // add a key-value pair to the context
    ctx := context.WithValue(r.Context(), "key", "my-value")
    // print current value for :uuid
    fmt.Fprintf(w, "Named parameter: %q, uuid; %q,  key: %s",
        params[":uuid"],
        uuid,
        ctx.Value("key"),
    )
}

func main() {
    router := violetear.New()

    router.AddRegex(":uuid", `[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}`)

    router.HandleFunc("*", catchAll)
    router.HandleFunc("/:uuid", handleUUID, "GET,HEAD")

    srv := &http.Server{
        Addr:           ":8080",
        Handler:        router,
        ReadTimeout:    5 * time.Second,
        WriteTimeout:   7 * time.Second,
        MaxHeaderBytes: 1 << 20,
    }
    log.Fatal(srv.ListenAndServe())
}

Duplicated named parameters

In cases where the same named parameter is used multiple times, example:

/test/:uuid/:uuid/

An slice is created, for getting the values you need to do something like:

params := r.Context().Value(violetear.ParamsKey).(violetear.Params)
uuid := params[":uuid"].([]string)

Notice the : prefix when getting the named_parameters

Or by using GetParams:

uuid := violetear.GetParams("uuid")

After this you can access the slice like normal:

fmt.Println(uuid[0], uuid[1])
Expand ▾ Collapse ▴

Documentation

Overview

    Package violetear - HTTP router

    Basic example:

    package main
    
    import (
       "fmt"
       "github.com/nbari/violetear"
       "log"
       "net/http"
    )
    
    func catchAll(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, r.URL.Path[1:])
    }
    
    func helloWorld(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, r.URL.Path[1:])
    }
    
    func handleUUID(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, r.URL.Path[1:])
    }
    
    func main() {
        router := violetear.New()
        router.LogRequests = true
        router.RequestID = "REQUEST_LOG_ID"
    
        router.AddRegex(":uuid", `[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}`)
    
        router.HandleFunc("*", catchAll)
        router.HandleFunc("/hello", helloWorld, "GET,HEAD")
        router.HandleFunc("/root/:uuid/item", handleUUID, "POST,PUT")
    
        srv := &http.Server{
            Addr:           ":8080",
            Handler:        router,
            ReadTimeout:    5 * time.Second,
            WriteTimeout:   7 * time.Second,
            MaxHeaderBytes: 1 << 20,
        }
        log.Fatal(srv.ListenAndServe())
    }
    

    Index

    Constants

    View Source
    const (
    	ParamsKey key = 0
    )

      ParamsKey used for the context

      Variables

      This section is empty.

      Functions

      func GetParam

      func GetParam(name string, r *http.Request, index ...int) string

        GetParam returns a value for the parameter set in path When having duplicate params pass the index as the last argument to retrieve the desired value.

        func GetParams

        func GetParams(name string, r *http.Request) []string

          GetParams returns param or params in a []string

          func GetRouteName

          func GetRouteName(r *http.Request) string

            GetRouteName return the name of the route

            Types

            type MethodHandler

            type MethodHandler struct {
            	Method  string
            	Handler http.Handler
            }

              MethodHandler keeps HTTP Method and http.handler

              type Params

              type Params map[string]interface{}

                Params string/interface map used with context

                func (Params) Add

                func (p Params) Add(k, v string)

                  Add param to Params

                  type ResponseWriter

                  type ResponseWriter struct {
                  	http.ResponseWriter
                  	// contains filtered or unexported fields
                  }

                    ResponseWriter wraps the standard http.ResponseWriter

                    func NewResponseWriter

                    func NewResponseWriter(w http.ResponseWriter, rid string) *ResponseWriter

                      NewResponseWriter returns ResponseWriter

                      func (*ResponseWriter) RequestID

                      func (w *ResponseWriter) RequestID() string

                        RequestID retrieve the Request ID

                        func (*ResponseWriter) RequestTime

                        func (w *ResponseWriter) RequestTime() string

                          RequestTime return the request time

                          func (*ResponseWriter) Size

                          func (w *ResponseWriter) Size() int

                            Size provides an easy way to retrieve the response size in bytes

                            func (*ResponseWriter) Status

                            func (w *ResponseWriter) Status() int

                              Status provides an easy way to retrieve the status code

                              func (*ResponseWriter) Write

                              func (w *ResponseWriter) Write(data []byte) (int, error)

                                Write satisfies the http.ResponseWriter interface and captures data written, in bytes

                                func (*ResponseWriter) WriteHeader

                                func (w *ResponseWriter) WriteHeader(statusCode int)

                                  WriteHeader satisfies the http.ResponseWriter interface and allows us to catch the status code

                                  type Router

                                  type Router struct {
                                  
                                  	// Logger
                                  	Logger func(*ResponseWriter, *http.Request)
                                  
                                  	// LogRequests yes or no
                                  	LogRequests bool
                                  
                                  	// NotFoundHandler configurable http.Handler which is called when no matching
                                  	// route is found. If it is not set, http.NotFound is used.
                                  	NotFoundHandler http.Handler
                                  
                                  	// NotAllowedHandler configurable http.Handler which is called when method not allowed.
                                  	NotAllowedHandler http.Handler
                                  
                                  	// PanicHandler function to handle panics.
                                  	PanicHandler http.HandlerFunc
                                  
                                  	// RequestID name of the header to use or create.
                                  	RequestID string
                                  
                                  	// Verbose
                                  	Verbose bool
                                  	// contains filtered or unexported fields
                                  }

                                    Router struct

                                    func New

                                    func New() *Router

                                      New returns a new initialized router.

                                      func (*Router) AddRegex

                                      func (r *Router) AddRegex(name, regex string) error

                                        AddRegex adds a ":named" regular expression to the dynamicRoutes

                                        func (*Router) GetError

                                        func (r *Router) GetError() error

                                          GetError returns an error resulted from building a route, if any.

                                          func (*Router) Handle

                                          func (r *Router) Handle(path string, handler http.Handler, httpMethods ...string) *Trie

                                            Handle registers the handler for the given pattern (path, http.Handler, methods).

                                            func (*Router) HandleFunc

                                            func (r *Router) HandleFunc(path string, handler http.HandlerFunc, httpMethods ...string) *Trie

                                              HandleFunc add a route to the router (path, http.HandlerFunc, methods)

                                              func (*Router) MethodNotAllowed

                                              func (r *Router) MethodNotAllowed() http.HandlerFunc

                                                MethodNotAllowed default handler for 405

                                                func (*Router) ServeHTTP

                                                func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request)

                                                  ServeHTTP dispatches the handler registered in the matched path

                                                  type Trie

                                                  type Trie struct {
                                                  	Handler     []MethodHandler
                                                  	HasCatchall bool
                                                  	HasRegex    bool
                                                  	Node        []*Trie
                                                  	// contains filtered or unexported fields
                                                  }

                                                    Trie data structure

                                                    func (*Trie) Get

                                                    func (t *Trie) Get(path, version string) (*Trie, string, string, bool)

                                                      Get returns a node

                                                      func (*Trie) Name

                                                      func (t *Trie) Name(name string) *Trie

                                                        Name add custom name to node

                                                        func (*Trie) Set

                                                        func (t *Trie) Set(path []string, handler http.Handler, method, version string) (*Trie, error)

                                                          Set adds a node (url part) to the Trie

                                                          func (*Trie) SplitPath

                                                          func (t *Trie) SplitPath(path string) (string, string)

                                                            SplitPath returns first element of path and remaining path

                                                            Directories

                                                            Path Synopsis
                                                            Package middleware - HTTP middleware https://github.com/justinas/alice Basic example: package main import ( "github.com/nbari/violetear" "github.com/nbari/violetear/middleware" "log" "net/http" ) func commonHeaders(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("X-app-Version", "1.0") next.ServeHTTP(w, r) }) } func middlewareOne(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { log.Println("Executing middlewareOne") next.ServeHTTP(w, r) log.Println("Executing middlewareOne again") }) } func main() { router := violetear.New() stdChain := middleware.New(commonHeaders, middlewareOne) router.Handle("/", stdChain.ThenFunc(catchAll), "GET,HEAD") log.Fatal(http.ListenAndServe(":8080", router)) }
                                                            Package middleware - HTTP middleware https://github.com/justinas/alice Basic example: package main import ( "github.com/nbari/violetear" "github.com/nbari/violetear/middleware" "log" "net/http" ) func commonHeaders(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("X-app-Version", "1.0") next.ServeHTTP(w, r) }) } func middlewareOne(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { log.Println("Executing middlewareOne") next.ServeHTTP(w, r) log.Println("Executing middlewareOne again") }) } func main() { router := violetear.New() stdChain := middleware.New(commonHeaders, middlewareOne) router.Handle("/", stdChain.ThenFunc(catchAll), "GET,HEAD") log.Fatal(http.ListenAndServe(":8080", router)) }