如何将Go中间件模式与错误返回请求处理程序结合起来?

时间:2017-03-18 06:17:48

标签: go middleware go-alice

我熟悉Go中间件模式:

// 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.
    })
}

例如,如果我有一个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))
    })
}

一个简单的handleFunc:

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

我可以将它们组合起来:

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

一切都很好。

但我喜欢Handlers能够像普通函数那样返回错误的想法。这使得错误处理变得更加容易,因为如果有错误我只能返回错误,或者只是在函数结束时返回nil。

我这样做了:

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
}

然后像这样包装它来使用它:

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

我可以将这两种模式分开工作,但我不知道如何将它们组合起来。我喜欢我能够用Alice这样的库链接中间件。但如果他们也可以返回错误那就太好了。我有办法实现这个目标吗?

4 个答案:

答案 0 :(得分:1)

我喜欢这种HandlerFuncs模式也返回错误,它更整洁,你只需编写一次错误处理程序。只需将中间件与其包含的处理程序分开考虑,您就不需要中间件来传递错误。中间件就像一个链依次执行每个中间件,然后最后一个中间件是一个知道你的处理程序签名的中间件,并适当地处理错误。

因此,在它最简单的形式中,保持中间件完全相同,但最后插入一个这种形式(并且不会执行另一个中间件,但是一个特殊的HandlerFunc): / p>

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

...

然后像这样包装你的函数:

moreMiddleware(myMiddleWare(errorHandler(myhandleFuncReturningError)))

这意味着这个特殊的错误中间件只能包装你的特殊功能签名,并且链接的末尾,但这很好。此外,我还考虑将此行为包装在您自己的多路复用器中,以使其更简单并避免传递错误处理程序,并让您更轻松地构建一系列中间件,而不会在路由设置中进行难看的包装。

我认为如果您正在使用路由器库,则需要明确支持此模式才能正常工作。您可以在此路由器中以修改后的形式查看此操作示例,该路由器完全使用您之后的签名,但处理构建中间件链并执行它而无需手动换行:

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

答案 1 :(得分:0)

根据定义,中间件的输出是HTTP响应。如果发生错误,它会阻止请求被执行,在这种情况下,中间件应该返回HTTP错误(如果服务器上出现意外错误,则返回500),或者它不会,在这种情况下,应该记录所发生的任何事情。它可以由系统管理员修复,执行应该继续。

如果你想通过允许你的功能恐慌来实现这一点(尽管我不建议这样做),抓住这种情况并在以后处理它而不会崩溃服务器,this blog post部分有一个例子恐慌恢复(甚至使用Alice)。

答案 2 :(得分:0)

根据我的理解,您希望将errorHandler功能链接起来,并将它们合并到loggingHandler中。

执行此操作的一种方法是使用structloggingHandler作为参数传递给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 } ,如下所示:

main

并在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()) } } 中将其称为:

{{1}}

答案 3 :(得分:0)

最灵活的解决方案是这样的:

首先定义一个与您的处理程序签名匹配的类型并实现 ServeHTTP 以满足 http.Handler 接口。通过这样做,ServeHTTP 将能够调用处理程序函数并在失败时处理错误。类似的东西:

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

现在像往常一样创建中间件。中间件应该创建一个函数,如果失败或调用链中的下一个成功则返回错误。然后将函数转换为定义的类型,例如:

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

现在您可以像使用任何常规中间件一样使用中间件。