Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Extending HTTP Handlers

Tags:

go

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?

like image 734
elithrar Avatar asked Feb 01 '14 08:02

elithrar


1 Answers

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))

like image 130
elithrar Avatar answered Oct 20 '22 00:10

elithrar