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

How can I combine Go middleware pattern with error returning request handlers?

我熟悉这样的 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 能够像普通函数一样处理 return 错误的想法。这使得错误处理变得更加容易,因为如果出现错误,我可以 return 一个错误,或者在函数末尾 return 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 这样的库链接在一起。但如果他们也能出现 return 错误就好了。我有办法实现这个目标吗?

根据定义,中间件的输出是 HTTP 响应。如果发生错误,它要么阻止请求得到满足,在这种情况下,中间件应该 return 一个 HTTP 错误(如果服务器上出现意外错误,则为 500),或者它不会,在这种情况下,无论发生什么应该记录下来,以便系统管理员可以修复它,并且应该继续执行。

如果你想通过让你的函数崩溃(虽然我不建议故意这样做)来实现这一点,捕获这种情况并在以后处理它而不会使服务器崩溃,this blog post 中有一个示例在 Panic Recovery 部分(它甚至使用 Alice)。

据我了解,您想链接 errorHandler 函数并将它们组合到 loggingHandler.

一种方法是使用 struct 将其作为参数传递给您的 loggingHandler,如下所示:

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

}

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

所以在最简单的形式中,保持你拥有的中间件完全相同,但在最后插入一个具有这种形式的中间件(并且不执行另一个中间件,而是执行一个特殊的 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
        }
    })
}

...

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

moreMiddleware(myMiddleWare(errorHandler(myhandleFuncReturningError)))

这意味着这个特殊的错误中间件只能包装您的特殊函数签名,并位于链的末尾,但这没关系。此外,我会考虑将此行为包装在您自己的多路复用器中,以使其更简单一些并避免传递错误处理程序,并让您更轻松地构建中间件链,而无需在路由设置中进行丑陋的包装。

我认为如果您使用路由器库,它可能需要明确支持此模式才能工作。您可以在这个路由器中以修改后的形式看到一个实际的例子,它完全使用您想要的签名,但处理构建中间件链并在没有手动包装的情况下执行它:

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

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

首先定义一个与你的处理程序签名相匹配的类型并实现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)
    }
}

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

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

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