-
Notifications
You must be signed in to change notification settings - Fork 6
Reasoning
Interceptors are a powerful tool that enables you to log, monitor, mutate, redirect and sometimes even fail a request or response. The beauty of interceptors is, that to the user everything looks as a regular *http.Client
.
In this article I will try to reason on how to build one.
Let's look at logging as an example, there are functions in "net/http/httputil" that allows you to dump requests and responses. Here is an example how one might use them
client := http.DefaultClient
request, _ := http.NewRequest(http.MethodGet, "https://www.golang.org", nil) // you should check for error here
// ### Outgoing Request Dump ###
bytes, _ := httputil.DumpRequestOut(request, true) // and here, don't ignore error
log.Printf("Request:\n\n%s\n", bytes)
// call remote
if response, err := client.Do(request); err == nil {
// ### Response Dump ###
bytes, _ = httputil.DumpResponse(response, true) // remember, don't ignore error
log.Printf("%s\n", bytes)
// do something with the response
...
}
Above example shows how one can use existing tools to log/debug what is sent and received when calling remote HTTP service. However you will need to write this code every time when using *http.Client
...
To solve this annoyance let's examine http.Client
type Client struct {
// Transport specifies the mechanism by which individual
// HTTP requests are made.
// If nil, DefaultTransport is used.
Transport RoundTripper
...
This http.RoundTripper
interface is the mechanism responsible for HTTP requests and it's defined as
type RoundTripper interface {
// RoundTrip executes a single HTTP transaction, returning
// a Response for the provided Request.
//
RoundTrip(*Request) (*Response, error)
}
Documentation was omitted for the purpose of this article, but you are strongly encouraged to read it.
That's exactly what we are looking for, a function with *http.Request
as an input and *http.Response
as output.
If we want to reuse our logging code from above we can do something that is similar to
type DumpingRoundTripper struct {
transport http.RoundTripper
}
func (dumper *DumpingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
bytes, _ := httputil.DumpRequestOut(req, true) // don't ignore error
log.Printf("%s\n", bytes)
response, err := dumper.transport.RoundTrip(req)
if err == nil {
bytes, _ = httputil.DumpResponse(response, true) // remember, don't ignore error
log.Printf("%s\n", bytes)
}
return response, err
}
func main() {
client := &http.Client{
Transport: &DumpingRoundTripper{
transport: http.DefaultTransport,
},
}
request, _ := http.NewRequest(http.MethodGet, "https://www.golang.org", nil) // you should check for error here
client.Do(request)
}
Congratulations! we just created an HTTP Client Interceptor, but it's tightly coupled to http.RoundTripper
Let's examine what we actually did. We implemented a function that receives a *http.Request
returns a *http.Response
and we also had a http.RoundTripper
that we could use to execute an actual call. Then we configured *http.Client
to use that wrapper as a Transport.
But what if we need different kinds of http.RoundTripper
or Interceptors ? If we continue with the above example we will need to 'merge' all of them into a single one. Let us think of a way to get around that.
We want to be able to define an interceptor as a standalone function so we can easily reuse it or swap between them. Our interceptor need to have access to
- Request
- Function that receives a Request and outputs a Response
To help us let's define 2 function types
- Handler
type Handler func(*http.Request) (*http.Response, error)
- Interceptor
type Interceptor func(*http.Request, Handler) (*http.Response, error)
Let's rewrite our above example with these 2 types in mind
type Handler func(*http.Request) (*http.Response, error)
type Interceptor func(*http.Request, Handler) (*http.Response, error)
type GeneralRoundTripper struct {
transport http.RoundTripper
interceptor Interceptor
}
func (general *GeneralRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
return general.interceptor(req, general.transport.RoundTrip)
}
As you can see, our Interceptor
type function is not coupled to http.RoundTripper
, hence we can introduce an interceptor function that is "stand-alone"
func DumpingInterceptor(req *http.Request, handler Handler) (*http.Response, error) {
bytes, _ := httputil.DumpRequestOut(req, true) // don't ignore error
log.Printf("%s\n", bytes)
response, err := handler(req)
if err == nil {
bytes, _ = httputil.DumpResponse(response, true) // remember, don't ignore error
log.Printf("%s\n", bytes)
}
return response, err
}
func main() {
client := &http.Client{
Transport: &GeneralRoundTripper{
transport: http.DefaultTransport,
interceptor: DumpingInterceptor,
},
}
request, _ := http.NewRequest(http.MethodGet, "https://www.golang.org", nil) // you should check for error here
client.Do(request)
}
func DumpingInterceptor
is a function that is unaware of what a http.RoundTripper
is.
Look above, at the beginning we showed how http.RoundTripper
is defined. It has one function
RoundTrip(*Request) (*Response, error)
This is exactly how we defined our Handler type. You can see how it's used here
...
func (general *GeneralRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
return general.interceptor(req, general.transport.RoundTrip)
}
...
That's it, now we have a way to define a standalone Interceptor.
But what about multiple Interceptors? We need to define a way to "chain" them so that our GeneralRoundTripper
will still work. Lucky for you there is a library to do just that...