11
votes

I am familiar with the Go middleware pattern like this:

// Pattern for writing HTTP middleware.
func middlewareHandler(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Our middleware logic goes here before executing application handler.
        next.ServeHTTP(w, r)
        // Our middleware logic goes here after executing application handler.
    })
}

So for example if I had a loggingHandler:

func loggingHandler(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Before executing the handler.
        start := time.Now()
        log.Printf("Strated %s %s", r.Method, r.URL.Path)
        next.ServeHTTP(w, r)
        // After executing the handler.
        log.Printf("Completed %s in %v", r.URL.Path, time.Since(start))
    })
}

And a simple handleFunc:

func handleFunc(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte(`Hello World!`))
}

I could combine them like this:

http.Handle("/", loggingHandler(http.HandlerFunc(handleFunc)))
log.Fatal(http.ListenAndServe(":8080", nil))

That is all fine.

But I like the idea of Handlers being able to return errors like normal functions do. This makes error handling much easier as I can just return an error if there is an error, or just return nil at the end of the function.

I have done it like this:

type errorHandler func(http.ResponseWriter, *http.Request) error

func (f errorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    err := f(w, r)
    if err != nil {
        // log.Println(err)
        fmt.Println(err)
        os.Exit(1)
    }
}

func errorHandle(w http.ResponseWriter, r *http.Request) error {
    w.Write([]byte(`Hello World from errorHandle!`))
    return nil
}

And then use it by wrapping it like this:

http.Handle("/", errorHandler(errorHandle))

I can make these two patterns work separately, but I don't know how I could combine them. I like that I am able to chain middlewares with a library like Alice. But it would be nice if they could return errors too. Is there a way for me to achieve this?

4

4 Answers

5
votes

I like this pattern of HandlerFuncs returning errors too, it's much neater and you just write your error handler once. Just think of your middleware separately from the handlers it contains, you don't need the middleware to pass errors. The middleware is like a chain which executes each one in turn, and then the very last middleware is one which is aware of your handler signature, and deals with the error appropriately.

So in it's simplest form, keep the middleware you have exactly the same, but at the end insert one which is of this form (and doesn't execute another middleware but a special HandlerFunc):

// Use this special type for your handler funcs
type MyHandlerFunc func(w http.ResponseWriter, r *http.Request) error


// Pattern for endpoint on middleware chain, not takes a diff signature.
func errorHandler(h MyHandlerFunc) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
       // Execute the final handler, and deal with errors
        err := h(w, r)
        if err != nil {
            // Deal with error here, show user error template, log etc
        }
    })
}

...

Then wrap your function like this:

moreMiddleware(myMiddleWare(errorHandler(myhandleFuncReturningError)))

That means this special error middleware can only ever wrap your special function signature, and come at the end of the chain, but that's fine. Also I'd consider wrapping this behaviour in your own mux to make it a little simpler and avoid passing error handlers around, and let you build a chain of middleware more easily without having ugly wrapping in your route setup.

I think if you're using a router library, it needs explicit support for this pattern to work probably. You can see an example of this in action in a modified form in this router, which uses exactly the signatures you're after, but handles building a middleware chain and executing it without manual wrapping:

https://github.com/fragmenta/mux/blob/master/mux.go

1
votes

The most flexible solution would be like this:

First define a type that matches your handler signature and implement ServeHTTP to satisfy the http.Handler interface. By doing so, ServeHTTP will be able to call the handler function and process the error if it fails. Something like:

type httpHandlerWithError func(http.ResponseWriter, *http.Request) error

func (fn httpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if err := fn(w, r); err != nil {
         http.Error(w, err.Message, err.StatusCode)
    }
}

Now create the middleware as usual. The middleware should create a function which returns an error if it fails or calls the next in the chain on success. Then convert the function to the defined type something like:

func AuthMiddleware(next http.Handler) http.Handler {

    // create handler which returns error
    fn := func(w http.ResponseWriter, r *http.Request) error {

        //a custom error value
        unauthorizedError := &httpError{Code: http.StatusUnauthorized, Message: http.StatusText(http.StatusUnauthorized)}

        auth := r.Header.Get("authorization")
        creds := credentialsFromHeader(auth)

        if creds != nil {
            return unauthorizedError
        }

        user, err := db.ReadUser(creds.username)
        if err != nil {
            return &httpError{Code: http.StatusInternalServerError, Message: http.StatusText(http.StatusInternalServerError)}
        }

        err = checkPassword(creds.password+user.Salt, user.Hash)
        if err != nil {
            return unauthorizedError
        }

        ctx := r.Context()
        userCtx := UserToCtx(ctx, user)

        // we got here so there was no error
        next.ServeHTTP(w, r.WithContext(userCtx))
        return nil
    }

    // convert function
    return httpHandlerWithError(fn)
}

Now you can use the middleware as you would use any regular middleware.

0
votes

The output of a middleware, by definition, is an HTTP response. If an error occurred, either it prevents the request from being fulfilled, in which case the middleware should return an HTTP error (500 if something unexpectedly went wrong on the server), or it does not, in which case whatever happened should be logged so that it can be fixed by a system administrator, and the execution should continue.

If you want to achieve this by allowing your functions to panic (although I would not recommend doing this intentionally), catching this situation and handling it later without crashing the server, there is an example in this blog post in section Panic Recovery (it even uses Alice).

0
votes

From what I understand you wanted to chain your errorHandler function and and combine them in your loggingHandler.

One way to do this is using a struct passing it to your loggingHandler as parameter like this :

func loggingHandler(errorHandler ErrorHandler, next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Call your error handler to do thing
        err := errorHandler.ServeHTTP()
        if err != nil {
            log.Panic(err)
        }

        // next you can do what you want if error is nil.
        log.Printf("Strated %s %s", r.Method, r.URL.Path)
        next.ServeHTTP(w, r)
        // After executing the handler.
        log.Printf("Completed %s in %v", r.URL.Path, time.Since(start))
    })
}

// create the struct that has error handler
type ErrorHandler struct {
}

// I return nil for the sake of example.
func (e ErrorHandler) ServeHTTP() error {
    return nil
}

and in the main you call it like this :

func main() {
    port := "8080"
    // you can pass any field to the struct. right now it is empty.
    errorHandler := ErrorHandler{}

    // and pass the struct to your loggingHandler.
    http.Handle("/", loggingHandler(errorHandler, http.HandlerFunc(index)))


    log.Println("App started on port = ", port)
    err := http.ListenAndServe(":"+port, nil)
    if err != nil {
        log.Panic("App Failed to start on = ", port, " Error : ", err.Error())
    }

}