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)
})
CodePudding user response:
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 toDirector
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 := &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
}
// further setup...
},
}
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.
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.