cancel a web request and handle errors inside the ReverseProxy Director function

Issue

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

Solution

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 := &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.


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.

Answered By – blackgreen

Answer Checked By – Robin (GoLangFix Admin)

Leave a Reply

Your email address will not be published.