Context-aware middleware chains for Golang net/http
MIT License
The release of Go 1.7 greatly simplified use of contexts in HTTP handlers. Further development of Noodle is continued in separate Github organization go-noodle, making this project and its branches obsolete. It is currently being maintained only to avoid breakage of old code. Please address your pull requests and issues to the new and improved Noodle project.
Noodle is a tiny and (almost) unopinionated Golang middleware stack. It borrows its ideas from Stack package, but relies on Golang net contexts for threading request environment through handler chains.
noodle.Middleware
is a generic func(Handler) Handler
bidirectional
middleware that accepts and returns noodle.Handler
. Following is an example
of HTTP basic auth middleware that stores user login in request context
.
// HTTPAuth is a middleware factory function that accepts `authFunc` for
// username and password verification and returns `noodle.Middleware`
func HTTPAuth(authFunc func(username, password string) bool) noodle.Middleware
{
// The middleware function verifies user credentials and aborts chain
// execution if `authFunc` returns `false`
return func(next noodle.Handler) noodle.Handler {
return func(c context.Context, w http.ResponseWriter, r *http.Request) error {
username, password, ok := r.BasicAuth()
if !ok || !authFunc(username, password) {
w.Header().Set("WWW-Authenticate", "Basic realm=Restricted")
w.WriteHeader(http.StatusUnauthorized)
// Provide error for logging middleware then abort chain
return fmt.Errorf("Unauthorized request")
}
// Inject user name into request context
userContext := context.WithValue(c, "User", username)
return next(userContext, w, r)
}
}
}
Note that similar HTTP Basic Auth middleware is included in middleware
subpackage discussed below.
noodle.Handler
provides context-aware http.Handler
with error
return
value for enhanced chaining. Assuming that some middleware stored user login in
request context, the following handler outputs personalized greeting:
func index(c context.Context, w http.ResponseWriter, r *http.Request) error {
user := c.Value("User").(string)
fmt.Fprintf(w, "Hello %s", user)
return nil
}
Middleware chains are created with noodle.New()
. Optionally middewares can be
passed to this function to initalize chain.
basicAuth := HTTPAuth(func(username, password string) bool {
return password == "secret"
})
n := noodle.New(basicAuth)
At any moment noodle.Chain
can be extended by calling Use()
method with
some additional middlewares as arguments. Each Use()
call creates new
middleware chain totally independent from parent. The following example extends
root chain with variables from gorilla/mux
router. For standalone example of
gorilla/mux
usage see provided sample code
and adapt/gorilla
subpackage.
func GorillaVars(next noodle.Handler) noodle.Handler {
return func(c context.Context, w http.ResponseWriter, r *http.Request) error {
withVars := context.WithValue(c, "Vars", mux.Vars(r))
return next(withVars, w, r)
}
}
n = n.Use(GorillaVars)
Middleware chain is finalized and converted to noodle.Handler
with Then()
method. Its first parameter is an application handler that consumes context and
serves user requests. The resulting handler implements http.Handler
interface
providing ServeHTTP
method. When serving HTTP from noodle.Handler
default
empty context is passed to each request. For further flexibility
noodle.Handler
can be provided with externally created context
. This
advanced usage is outlined in
httprouter adaptor example
and put to use in wok
router .
func index(c context.Context, w http.ResponseWriter, r *http.Request) error {
fmt.Fprintln(w, "index")
return nil
}
...
http.Handle("/", n.Then(index))
Package noodle
comes with a collection of essential middlewares organized
into the middleware
package. Import "github.com/andviro/noodle/middleware" to
get access to following:
Logger
provides basic request logging with some useful info about response
timing. Recovery
middleware wraps the panic object into error
and passes it
further for logger middleware to display.
import (
"github.com/andviro/noodle/middleware"
)
func panickyIndex(c context.Context, w http.ResponseWriter, r *http.Request) error {
...
panic("Oh noes!!!")
...
}
...
n := noodle.New(middleware.Logger, middleware.Recover)
http.Handle("/", n.Then(panickyIndex))
LocalStore
middleware injects a thread-safe data store into the request
context. This store can be used to pass variables back and forth along the
middleware chain. Consider a handler that sets some variable in request-local
store and a middleware that wraps that handler and uses that variable after
handler execution to render output in JSON format:
func RenderJSON(next noodle.Handler) noodle.Nandler {
return func(c context.Context, w http.ResponseWriter, r *http.Request) error {
if err := next(c, w, r); err != nil {
return err
}
// Expect some "data" value in local store
data := middleware.GetStore(c).MustGet("data")
json.NewEncoder(w).Encode(data)
return nil
}
}
func index(c context.Context, w http.ResponseWriter, r *http.Request) error {
var res struct {
A int
B string
}{1, "Ahahaha"}
// This value will be caught by RenderJson middleware
middleware.GetStore(c).Set("data", &res)
return nil
}
...
n := noodle.New(middleware.LocalStore, RenderJSON)
http.Handle("/", n.Then(index))
For convenience, initial noodle.Chain
with logging, recovery and
request-local store can be created with middleware.Default()
constructor.
Refer to package documentation for further information on provided middlewares.
Package render provides
basic middleware for serialization of handler-supplied values, similar to the
example above. The only difference is that handler must call render.Yield
function to pass HTTP status code and its data back to the render middleware
through context. Currently supported are render.JSON
middleware for JSON
serialization, render.XML
for XML output and render.Template
that uses
pre-compiled html/template
object to render data object into HTML.
import (
mw "github.com/andviro/noodle/middleware"
"github.com/andviro/noodle/render"
"html/template"
)
type TestStruct struct {
A int `json:"a"`
B string `json:"b"`
}
func index(c context.Context, w http.ResponseWriter, r *http.Request) error {
testData := TestStruct{1, "Ohohoho"}
return render.Yield(c, 201, &testData)
})
...
n := mw.Default()
http.Handle("/jsonEndpoint", n.Use(render.JSON).Then(index))
tpl, _ := template.New().Parse("<h1>Hello {{ .B }}</h1> {{ .A }}")
http.Handle("/htmlEndpoint", n.Use(render.Template(tpl)).Then(index))
render.ContentType
analyzes request's Accept
header and renders output data
to JSON, XML or HTML, setting ContentType
appropriately. If template for HTML
rendering is nil
, result is rendered to indented JSON inside HTML PRE
tag,
which can be used for endpoint debugging. If Accept
header is not specified
or not recognized, the result is rendered to JSON.
tpl, _ := template.Must(template.New().Parse("<h1>Hello {{ .B }}</h1> {{ .A }}"))
// or
// tpl := nil
http.Handle("/anyContent", n.Use(render.ContentType(tpl)).Then(index))
Package bind provides
middleware for loading request body into supplied model. Handlers retrieve
bound objects using bind.GetData
function.
import (
mw "github.com/andviro/noodle/middleware"
"github.com/andviro/noodle/bind"
)
type TestStruct struct {
A int `json:"a" form:"a"`
B string `json:"b" form:"b"`
}
func index(c context.Context, w http.ResponseWriter, r *http.Request) error {
data := bind.GetData(c).(*TestStruct) // Get parsed data from context
// Use model data
...
return nil
})
...
n := mw.Default()
// The following handler will bind request body to TestStruct type
http.Handle("/jsonPostEndpoint", n.Use(bind.JSON(TestStruct{})).Then(index))
// The following handler will bind post form to TestStruct type
http.Handle("/formPostEndpoint", n.Use(bind.Form(TestStruct{})).Then(index))
Currently binding of JSON and web forms through agj/form library is supported. XML etc is work in progress, PRs are appreciated.
Subpackage adapt
contains adaptors for third-party middleware libraries. adapt.Http
converts
generic middleware constructor with signature func(http.Handler) http.Handler
to noodle.Middleware
. Resulting constructor can be easily integrated into
existing noodle.Chain
with Use
method. While converted middleware can not
consume request context and is not able to return any error, context
propagation is not broken and error values will bubble up from further handlers
in chain. This allows usage of various middlewares written for third-party
middleware libraries, like
interpose.
import (
"github.com/andviro/noodle"
"github.com/andviro/noodle/adapt"
)
func DumbMid(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.RequestWriter, r *http.Request){
fmt.Fprintf(w, "I'm dumb and proud of it!!!")
next.ServeHTTP(w, r)
})
}
...
// AwareMid2 and indexHandler will consume context from AwareMid1
n := noodle.New(AwareMid1, adapt.Http(DumbMid), AwareMid2)
http.Handle("/", n.Then(indexHandler))
adapt.Negroni
creates noodle.Middleware
from function with signature
func(http.ResponseWriter, *http.Request, http.HandlerFunc)
. This adaptor
simplifies integration of middlewares written for
negroni package.
func NegroniHandler (w http.RequestWriter, r *http.Request, next http.HandlerFunc) {
fmt.Fprintf(w, "Hi, I'm a Negroni middleware!!!")
next(w, r)
}
...
// AwareMid2 will consume context from AwareMid1
n := noodle.New(AwareMid1, adapt.Negroni(NegroniHandler), AwareMid2)
http.Handle("/", n.Then(indexHandler))
For compatibility with Gorilla mux corresponding middleware is provided.
This code is released under MIT license.