英文:
cancel a web request and handle errors inside the ReverseProxy Director function
问题
我在想是否可以在ReverseProxy.Director函数内部取消一个网络请求或向客户端发送内部响应。
假设我们执行了某些会抛出错误的操作,或者有其他原因不希望转发请求。
proxy := &httputil.ReverseProxy{
Director: func(r *http.Request) {
err := somethingThatThrows()
},
}
http.Handle("/", proxy)
下面是一种解决方案,但它不如上述使用代理的方式简洁。我也不确定应该在多大程度上修改请求。似乎应该在Director函数中进行这样的操作。
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
err := somethingThatThrows()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
proxy.ServeHTTP(w, r)
})
英文:
I am wondering if it would be possible to cancel a web request or send an internal response to the client inside the ReverseProxy.Director function.
Suppose we do something that throws an error, or we have other reason to not forward the request.
proxy := &httputil.ReverseProxy{
Director: func(r *http.Request) {
err := somethingThatThrows()
},
}
http.Handle("/", proxy)
A solution to this might be the below, but it's not as neat as the above way to use the proxy. I am also not sure to which degree the request should be modified that way. The director seems to be the place to do that.
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
err := somethingThatThrows()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
proxy.ServeHTTP(w, r)
})
答案1
得分: 3
如果可能的话,可以取消一个网络请求。你可以取消传递给Director函数的请求,但是有一些细节需要考虑:
- 取消请求的正确方式是取消其上下文。
- 你不能取消没有自己设置(截止时间|超时|取消函数)的上下文 → 也就是说,你必须有访问
cancel函数的权限 → 也就是说,你不能取消由其他人创建的父级上下文。 - 传递给
Director函数的*http.Request是原始请求的克隆。
根据上述要点,你可以将Director中的请求替换为具有可取消上下文的另一个请求。代码示例如下:
proxy := &httputil.ReverseProxy{
Director: func(req *http.Request) {
// 创建可取消上下文,并重新设置请求
ctx, cancel := context.WithCancel(req.Context())
*req = *req.WithContext(ctx)
err := somethingThatThrows()
if err != nil {
cancel()
return
}
},
}
然后,上述代码本身不会执行任何其他操作。应该发生的是,实现了http.RoundTripper的httputil.ReverseProxy.Transport函数在实际发送任何内容到上游服务之前,检查请求上下文是否被取消。
Director的文档说明如下:
Director必须是一个将请求修改为使用Transport发送的新请求的函数。
当未提供Transport时,它将回退到http.DefaultTransport,当上下文被取消时会中止请求。当前代码(Go 1.17.5)如下所示:
select {
case <-ctx.Done():
req.closeBody()
return nil, ctx.Err()
default:
}
如果你提供自己的http.RoundTripper实现,你可能希望自己实现该行为。还要记住,如果上下文不可取消,上下文完成通道为nil,因此你必须设置取消函数并调用cancel(),以便让该select运行“完成”分支。
或者在ReverseProxy.Director内部向客户端发送内部响应。
根据上述文档中的同一引用,你不应该在Director函数内部向http.ResponseWriter写入内容,即使你在其周围进行了封闭。正如你所看到的,Director本身并没有将http.ResponseWriter作为参数传递,这应该已经是一个不言自明的细节。
如果你想在无法转发请求的情况下指定其他行为,并且假设无论http.RoundTripper的实现如何,当请求上下文被取消时返回error,你可以提供自己的ReverseProxy.ErrorHandler函数:
proxy.ErrorHandler = func(writer http.ResponseWriter, request *http.Request, err error) {
// 检查错误
// 向writer写入内容
}
当Transport返回错误时,包括来自取消请求的错误时,将调用ErrorHandler,并且它确实将http.ResponseWriter作为参数。
英文:
> if it would be possible to cancel a web request [...]
You can cancel the request that is passed to the Director function, BUT there are some details to consider:
- the correct way to cancel a request is to cancel its context
- you can not cancel contexts where you didn't set a (deadline|timeout|cancelfunc) yourself → i.e. you must have access to the
cancelfunction → i.e. you can't cancel parent contexts created by someone else. - the
*http.Requestpassed toDirectorfunction is a clone of the original request
Based on the points above, you can replace the request in the Director with another one that has a cancellable context. It may look like the following:
proxy := &httputil.ReverseProxy{
Director: func(req *http.Request) {
// create a cancellable context, and re-set the request
ctx, cancel := context.WithCancel(req.Context())
*req = *req.WithContext(ctx)
err := somethingThatThrows()
if err != nil {
cancel()
return
}
},
}
Then the code above doesn't do anything else by itself. What should happen is that the httputil.ReverseProxy.Transport function, which implements http.RoundTripper checks whether the request context is cancelled, before actually send anything to the upstream service.
The documentation of Director states:
> Director must be a function which modifies the request into a new request to be sent using Transport.
When the Transport is not provided, it will fall back to http.DefaultTransport, which aborts the request when the context is cancelled. The current code (Go 1.17.5) looks like:
select {
case <-ctx.Done():
req.closeBody()
return nil, ctx.Err()
default:
}
If you provide your own implementation of http.RoundTripper you may want to implement that behavior yourself. Remember also that the context done channel is nil if it's not cancellable, so you have to set a cancel func and call cancel() in order to have that select run the "done" case.
<hr>
> or send an internal response to the client inside the ReverseProxy.Director
Based on the same quote above docs, you should not write to the http.ResponseWriter from within the Director function — assuming you are even closing around it. As you can see the Director itself doesn't get the http.ResponseWriter as an argument, and this should already be a self-explanatory detail.
If you want to specify some other behavior in case the request can't be forwarded, and assuming that whatever implementation of http.RoundTripper is returning error when the req context is cancelled, you can provide your ReverseProxy.ErrorHandler function:
proxy.ErrorHandler = func(writer http.ResponseWriter, request *http.Request, err error) {
// inspect err
// write to writer
}
The ErrorHandler will be invoked when Transport returns error, including when the error comes from a cancelled request, and it does have http.ResponseWriter as an argument.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。


评论