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

huangapple go评论91阅读模式
英文:

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

问题

我熟悉类似这样的Go中间件模式:

// 用于编写HTTP中间件的模式。
func middlewareHandler(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// 在执行应用程序处理程序之前,我们的中间件逻辑放在这里。
		next.ServeHTTP(w, r)
		// 在执行应用程序处理程序之后,我们的中间件逻辑放在这里。
	})
}

例如,如果我有一个loggingHandler:

func loggingHandler(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// 在执行处理程序之前。
		start := time.Now()
		log.Printf("Started %s %s", r.Method, r.URL.Path)
		next.ServeHTTP(w, r)
		// 在执行处理程序之后。
		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))

这一切都很好。

但是,我喜欢处理程序能够像普通函数一样返回错误的想法。这样做可以使错误处理更加容易,因为我可以在函数末尾返回一个错误,或者如果有错误就返回一个错误,或者只返回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的库链接中间件。但如果它们也能返回错误就更好了。有没有办法实现这一点?

英文:

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?

答案1

得分: 6

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

所以,最简单的形式是保持你现有的中间件完全相同,但在最后插入一个具有以下形式的中间件(不执行另一个中间件,而是执行一个特殊的HandlerFunc):

// 为你的处理函数使用这个特殊类型
type MyHandlerFunc func(w http.ResponseWriter, r *http.Request) error

// 中间件链上的端点模式,接收不同的签名
func errorHandler(h MyHandlerFunc) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 执行最终的处理函数,并处理错误
        err := h(w, r)
        if err != nil {
            // 在这里处理错误,显示用户错误模板,记录日志等
        }
    })
}

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

moreMiddleware(myMiddleWare(errorHandler(myhandleFuncReturningError)))

这意味着这个特殊的错误中间件只能包装你特殊的函数签名,并且放在链条的最后,但这没关系。此外,我建议将这种行为封装在自己的mux中,这样可以更简单地构建中间件链,避免在路由设置中进行丑陋的包装,并且不需要传递错误处理程序。

我认为,如果你使用的是一个路由库,它需要明确支持这种模式才能正常工作。你可以在这个修改过的路由器中看到这种模式的实际应用示例,它使用了你所需要的确切签名,但处理了构建中间件链并执行它而无需手动包装的问题:

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

英文:

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

答案2

得分: 1

最灵活的解决方案如下:

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

    // 创建返回错误的处理程序
    fn := func(w http.ResponseWriter, r *http.Request) error {

        // 自定义错误值
        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)

        // 到达此处表示没有错误
        next.ServeHTTP(w, r.WithContext(userCtx))
        return nil
    }

    // 转换函数
    return httpHandlerWithError(fn)
}

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

英文:

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.

答案3

得分: 0

中间件的输出,根据定义,是一个HTTP响应。如果发生错误,要么阻止请求被满足,在这种情况下,中间件应返回一个HTTP错误(如果服务器发生了意外错误,则返回500),要么不阻止请求,但是应该将发生的情况记录下来,以便系统管理员进行修复,并且执行应该继续进行。

如果你想通过允许函数发生panic的方式来实现这一点(尽管我不建议故意这样做),并且在稍后处理这种情况而不使服务器崩溃,可以参考这篇博文中的Panic Recovery部分(它甚至使用了Alice)。

英文:

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

答案4

得分: 0

根据我理解,您想要将您的errorHandler函数链接起来,并将它们组合在loggingHandler中。

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

func loggingHandler(errorHandler ErrorHandler, next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 调用您的错误处理程序执行操作
        err := errorHandler.ServeHTTP()
        if err != nil {
            log.Panic(err)
        }
        
        // 如果错误为nil,您可以执行其他操作
        log.Printf("Strated %s %s", r.Method, r.URL.Path)
        next.ServeHTTP(w, r)
        // 执行处理程序后
        log.Printf("Completed %s in %v", r.URL.Path, time.Since(start))
    })
}

// 创建具有错误处理程序的结构体
type ErrorHandler struct {
}

// 为了示例,我返回nil
func (e ErrorHandler) ServeHTTP() error {
    return nil
}

main函数中,您可以这样调用它:

func main() {
    port := "8080"
    // 您可以向结构体传递任何字段,现在它是空的
    errorHandler := ErrorHandler{}

    // 将结构体传递给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())
    }
}

希望对您有所帮助!

英文:

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

}

huangapple
  • 本文由 发表于 2017年3月18日 14:17:47
  • 转载请务必保留本文链接:https://go.coder-hub.com/42871194.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定