取消一个网络请求并在ReverseProxy Director函数内处理错误。

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

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.RoundTripperhttputil.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 cancel function → i.e. you can't cancel parent contexts created by someone else.
  • the *http.Request passed to Director function 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 := &amp;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 &lt;-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.

huangapple
  • 本文由 发表于 2022年2月8日 17:30:40
  • 转载请务必保留本文链接:https://go.coder-hub.com/71031456.html
匿名

发表评论

匿名网友

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

确定