Extending HTTP Handlers

go

Solution

I've managed to solve this thanks to the help of 'cronos' on #go-nuts.

The solution allows me to use a custom handler type, chain middleware and avoid the repetition of having to wrap handlers (i.e. appHandler(myHandler)), middleware...):

type appHandler func(http.ResponseWriter, *http.Request) *appError

type appError struct {
    Code  int
    Error error
}

func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if e := fn(w, r); e != nil {

        switch e.Code {

        case http.StatusNotFound:
            notFound(w, r)
        case http.StatusInternalServerError:
            serverError(w, r, e.Error, e.Code)
        default:
            serverError(w, r, e.Error, e.Code)
        }
    }
}

func use(h appHandler, middleware ...func(http.Handler) http.Handler) http.Handler {
    var res http.Handler = h
    for _, m := range middleware {
        res = m(res)
    }

    return res
}

func someMiddleware(h http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

        w.Header().Set("Cache-Control", "max-age=0, private, must-revalidate")
        w.Header().Set("X-Accel-Expires", "0")
        h.ServeHTTP(w, r)
    })
}

func myHandler(w http.ResponseWriter, r *http.Request) *appError {

    err := doSomething()
    if err != nil {
        return &appError{500, err}
    }

    // render your template, etc.
    return nil
}

With routes looking like this: `r.Handle("/route", use(myHandler, someMiddleware))`

You can obviously modify `appHandler` to return whatever you like, add additional fields to `appError` and so on. Your middleware is also able to wrap your router if you want to apply it to all routes - i.e. `http.Handle("/", someMiddleware(r))`

Problem

I have a fairly quick-and-dirty error handler in my Go web app that raises a HTTP error, logs the important parts of the response and serves an error template. I'd like to remove the repetition where I'm writing something like this a few too many times in a handler: ``` err := doSomething() if err != nil { serverError(w, r, err, code) } ``` I've had a good read of the Error Handling and Go article which covers defining a custom HTTP handler type that returns a error type/struct like this (or even returning int, err instead): ``` type appHandler func(http.ResponseWriter, *http.Request) *appError type appError struct { code int Err error } // Ensures appHandler satisfies the http.Handler interface func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if err := fn(w, r); err != nil { switch err.Code { case http.StatusNotFound: http.NotFound(w, r) case http.StatusInternalServerError: http.Error(w, "message", http.StatusInternalServerError) default: http.Error(w, "message", err.Code) } } } ``` But I'm not sure how to retain my existing middleware functionality/wrapper that allows me to chain middleware like this: `r.HandleFunc("/route", use(myHandler, middleware1, middleware2))` where `use` and my middleware look like this: ``` func use(h http.HandlerFunc, middleware ...func(http.HandlerFunc) http.HandlerFunc) http.HandlerFunc { for _, m := range middleware { h = m(h) } return h } func AntiCSRF(h http.HandlerFunc) http.HandlerFunc { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // do something // h.ServeHTTP(w,r) } } ``` From what I can figure, it'd be something like the below (which doesn't work). I'm getting an error saying `cannot use m(h) (type http.Handler) as type appHandler in assignment: need type assertion`. How do I resolve this whilst still keeping the middleware itself "as is"? You can find a (simplified) playground example here: http://play.golang.org/p/Cmmo-wK2Af ``` r.Handle("/route", use(myHandler, middleware.NoCache)) // Contrived example! func use(h myHandlerType?, middleware ...func(http.Handler) http.Handler) http.Handler { for _, m := range middleware { h = m(h) } return h } func myHandler(w http.ResponseWriter, r *http.Request) *appError { // Extremely contrived example name := "Matt" _, err := fmt.Fprintf(w, "Hi %s", name) if err != nil { return &appError{500, err} } return nil } func contrivedMiddleware(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "max-age=0, private, must-revalidate") w.Header().Set("X-Accel-Expires", "0") h.ServeHTTP(w, r) }) } ``` What am I missing and is there a better way to do this?

Original source