-
Notifications
You must be signed in to change notification settings - Fork 18.4k
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:
RoundTripper
is an interface representing the ability to execute a single HTTP transaction.Transport
is an implementation ofRoundTripper
. It manages a pool of cached connections.Client
is a concrete type which wraps aRoundTripper
. It adds cookie management and redirect following handling.ClientConn
is a proposed new addition (see proposal: net/http: client connection API #75772). This would be aRoundTripper
implementation 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
HTTPClient
interface defined in the same package. - Decouple client transports from
*http.Client
by accepting an interface modelcontextprotocol/go-sdk#522 contains some discussion on whether an API should accept an*http.Client
or 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.