-
Notifications
You must be signed in to change notification settings - Fork 18.5k
Description
The net/http package has three (possibly soon to be four) types representing an HTTP client, in the sense of something which can send an HTTP request and receive a response:
RoundTripperis an interface representing the ability to execute a single HTTP transaction.Transportis an implementation ofRoundTripper. It manages a pool of cached connections.Clientis a concrete type which wraps aRoundTripper. It adds cookie management and redirect following handling.ClientConnis a proposed new addition (see net/http: client connection API #75772). This would be aRoundTripperimplementation that represents a single connection.
The RoundTripper interface permits middleware to wrap a Transport or other RoundTripper and add additional functionality. For example, oauth2.Transport is a RoundTripper that adds "Authorization" headers to outbound requests.
While useful in some situations, the RoundTripper layer is the wrong place to add some types of functionality. For example, the oauth2.Transport I just mentioned can cause incorrect behavior when following a redirect: When an http.Client follows a cross-domain redirect, it strips sensitive headers (including "Authorization") from the redirected request. However, oauth2.Transport adds this header at a lower level than http.Client and has no way to identify redirect requests. Therefore, the "Authorization" header is still sent after a cross-domain redirect. (This is golang/oauth2#500, and I don't see any good way to fix it in the context of the current oauth2 package API.)
Middleware which acts on an entire request (including redirects) as opposed to an individual transaction needs to wrap the http.Client, not an RoundTripper.
Client is a concrete type. An API which wants to accept a user-provided client either accepts a concrete *http.Client (which precludes client-level middleware) or needs to define some additional interface. For example:
- https://pkg.go.dev/github.com/aws/aws-sdk-go/aws#Config.HTTPClient is a concrete
*http.Client. - https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/aws#Config.HTTPClient is an
HTTPClientinterface defined in the same package. - Decouple client transports from
*http.Clientby accepting an interface modelcontextprotocol/go-sdk#522 contains some discussion on whether an API should accept an*http.Clientor some locally-defined interface type.
We could add a new interface type to net/http that is implemented by *Client, and encourage APIs to accept this type rather than a concrete *Client. However, this approach doesn't do anything to help existing APIs which take a concrete *Client. In addition, *Client has six methods; while only one of them (Client.Do) is necessary for this use case, adding an interface type that is less useful than *Client itself seems unfortunate.
I instead propose that we add a new field to Client to provide a place to insert request-modifying middleware:
type Client struct {
// PrepareRequest specifies a per-request hook.
// If PrepareRequest is not nil, the client calls it before sending a request.
// It is not called after redirects.
//
// PrepareRequest returns the request to send or an error to terminate the request.
// If PrepareRequest needs to modify the request, it should clone the
PrepareRequest func(*Request) (*Request, error)
// ...existing fields
}The PrepareRequest hook gives us a way to inject middleware layers into any code which currently uses an *http.Client. For example, it would let us change oauth2.Config.Client to return an *http.Client that injects an Authorization header, but which still strips the header after cross-domain redirects.