Interceptors
Interceptors can be used to provide custom logic that runs prior to sending the request and/or after receiving a response. Use-cases for interceptors include:
- logging
- observabilitiy
- retries
- circuit breaking.
Specifically, an interceptor is a function that wraps the logic for making an HTTP request. Interceptors can be used to modify the request before it’s sent, modify the response before it’s returned, or to perform some action before or after the request is made.
Creating an Interceptor
Interceptors can be implemented by writing a function that conforms to the HandleFunc
type or by creating a struct that implements the Interceptor
interface.
HandleFunc
Approach
HandleFunc
is a function type that matches the signature of the Handle
method in the Interceptor
interface. This approach is ideal for simple interceptors or when you don’t need to maintain state between requests.
type HandleFunc func(ctx context.Context, req *http.Request, next Interceptor) (*http.Response, error)
Here’s an example of a simple interceptor that adds an Authorization
header that’s computed based on attributes of the request (e.g. path, request body etc.):
auth := httpr.HandleFunc(func(ctx context.Context, req *http.Request, next httpr.Interceptor) (*http.Response, error) { // Compute the signature signature := computeSignature(req)
// Add the Authorization header req.Header.Set("Authorization", signature)
// Call the next interceptor in the chain return next.Handle(ctx, req, next)})
client := httpr.NewClient(httpr.Intercept(auth))
Interceptor
Interface Approach
The Interceptor
interface is more suitable when you need to maintain state or when you’re creating a more complex interceptor that might benefit from being a struct with methods.
type Interceptor interface { Handle(ctx context.Context, req *http.Request, next Interceptor) (*http.Response, error)}
Here’s an example of a logging interceptor that logs the request method and URL before making the request:
package main
import ( "context" "fmt" "io" "log" "net/http" "os"
"github.com/mistermoe/httpr")
type RequestLogger struct { Logger *log.Logger}
func (l *RequestLogger) Handle(ctx context.Context, req *http.Request, next httpr.Interceptor) (*http.Response, error) { l.Logger.Printf("Sending %s request to %s\n", req.Method, req.URL) return next.Handle(ctx, req, next)}
func main() { logger := &RequestLogger{Logger: log.New(os.Stdout, "", 0)}
client := httpr.NewClient(httpr.Intercept(logger))
resp, err := client.Get(context.Background(), "https://httpbin.org/get") if err != nil { fmt.Printf("Error: %v\n", err) //nolint:forbidigo // example return } defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body) fmt.Printf("Response: %s\n", body) //nolint:forbidigo // example}
Using Interceptors
Interceptors can be provided to the client using the Intercept
option. You can provide multiple interceptors to the client like so:
// assume auth and logger are previously defined interceptors
httpc := httpr.NewClient( httpr.Intercept(auth), httpr.Intercept(logger),)
Interceptors can also be provided to individual requests using the Intercept
option:
// assume auth is a previously defined interceptor
httpc := httpr.NewClient()httpr.Get(context.Background(), "https://api.example.com", httpr.Intercept(auth))