Skip to content

Commit 39f4588

Browse files
authored
routing/http/client: avoid race by not using global http.Client
1 parent 79cb4e2 commit 39f4588

File tree

3 files changed

+40
-22
lines changed

3 files changed

+40
-22
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ The following emojis are used to highlight certain changes:
2929

3030
- 🛠 `boxo/gateway`: when making a trustless CAR request with the "entity-bytes" parameter, using a negative index greater than the underlying entity length could trigger reading more data than intended
3131
- 🛠 `boxo/gateway`: the header configuration `Config.Headers` and `AddAccessControlHeaders` has been replaced by the new middleware provided by `NewHeaders`.
32+
- 🛠 `routing/http/client`: the default HTTP client is no longer a global singleton. Therefore, using `WithUserAgent` won't modify the user agent of existing routing clients. This will also prevent potential race conditions. In addition, incompatible options will now return errors instead of silently failing.
3233

3334
### Security
3435

routing/http/client/client.go

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,8 @@ import (
2828
)
2929

3030
var (
31-
_ contentrouter.Client = &Client{}
32-
logger = logging.Logger("routing/http/client")
33-
defaultHTTPClient = &http.Client{
34-
Transport: &ResponseBodyLimitedTransport{
35-
RoundTripper: http.DefaultTransport,
36-
LimitBytes: 1 << 20,
37-
UserAgent: defaultUserAgent,
38-
},
39-
}
31+
_ contentrouter.Client = &Client{}
32+
logger = logging.Logger("routing/http/client")
4033
)
4134

4235
const (
@@ -67,53 +60,75 @@ var defaultUserAgent = moduleVersion()
6760

6861
var _ contentrouter.Client = &Client{}
6962

63+
func newDefaultHTTPClient(userAgent string) *http.Client {
64+
return &http.Client{
65+
Transport: &ResponseBodyLimitedTransport{
66+
RoundTripper: http.DefaultTransport,
67+
LimitBytes: 1 << 20,
68+
UserAgent: userAgent,
69+
},
70+
}
71+
}
72+
7073
type httpClient interface {
7174
Do(req *http.Request) (*http.Response, error)
7275
}
7376

74-
type Option func(*Client)
77+
type Option func(*Client) error
7578

7679
func WithIdentity(identity crypto.PrivKey) Option {
77-
return func(c *Client) {
80+
return func(c *Client) error {
7881
c.identity = identity
82+
return nil
7983
}
8084
}
8185

86+
// WithHTTPClient sets a custom HTTP Client to be used with [Client].
8287
func WithHTTPClient(h httpClient) Option {
83-
return func(c *Client) {
88+
return func(c *Client) error {
8489
c.httpClient = h
90+
return nil
8591
}
8692
}
8793

94+
// WithUserAgent sets a custom user agent to use with the HTTP Client. This modifies
95+
// the underlying [http.Client]. Therefore, you should not use the same HTTP Client
96+
// with multiple routing clients.
97+
//
98+
// This only works if using a [http.Client] with a [ResponseBodyLimitedTransport]
99+
// set as its transport. Otherwise, an error will be returned.
88100
func WithUserAgent(ua string) Option {
89-
return func(c *Client) {
101+
return func(c *Client) error {
90102
if ua == "" {
91-
return
103+
return errors.New("empty user agent")
92104
}
93105
httpClient, ok := c.httpClient.(*http.Client)
94106
if !ok {
95-
return
107+
return errors.New("the http client of the Client must be a *http.Client")
96108
}
97109
transport, ok := httpClient.Transport.(*ResponseBodyLimitedTransport)
98110
if !ok {
99-
return
111+
return errors.New("the transport of the http client of the Client must be a *ResponseBodyLimitedTransport")
100112
}
101113
transport.UserAgent = ua
114+
return nil
102115
}
103116
}
104117

105118
func WithProviderInfo(peerID peer.ID, addrs []multiaddr.Multiaddr) Option {
106-
return func(c *Client) {
119+
return func(c *Client) error {
107120
c.peerID = peerID
108121
for _, a := range addrs {
109122
c.addrs = append(c.addrs, types.Multiaddr{Multiaddr: a})
110123
}
124+
return nil
111125
}
112126
}
113127

114128
func WithStreamResultsRequired() Option {
115-
return func(c *Client) {
129+
return func(c *Client) error {
116130
c.accepts = mediaTypeNDJSON
131+
return nil
117132
}
118133
}
119134

@@ -122,13 +137,16 @@ func WithStreamResultsRequired() Option {
122137
func New(baseURL string, opts ...Option) (*Client, error) {
123138
client := &Client{
124139
baseURL: baseURL,
125-
httpClient: defaultHTTPClient,
140+
httpClient: newDefaultHTTPClient(defaultUserAgent),
126141
clock: clock.New(),
127142
accepts: strings.Join([]string{mediaTypeNDJSON, mediaTypeJSON}, ","),
128143
}
129144

130145
for _, opt := range opts {
131-
opt(client)
146+
err := opt(client)
147+
if err != nil {
148+
return nil, err
149+
}
132150
}
133151

134152
if client.identity != nil && client.peerID.Size() != 0 && !client.peerID.MatchesPublicKey(client.identity.GetPublic()) {

routing/http/client/client_test.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,11 +109,10 @@ func makeTestDeps(t *testing.T, clientsOpts []Option, serverOpts []server.Option
109109
server := httptest.NewServer(recordingHandler)
110110
t.Cleanup(server.Close)
111111
serverAddr := "http://" + server.Listener.Addr().String()
112-
recordingHTTPClient := &recordingHTTPClient{httpClient: defaultHTTPClient}
112+
recordingHTTPClient := &recordingHTTPClient{httpClient: newDefaultHTTPClient(testUserAgent)}
113113
defaultClientOpts := []Option{
114114
WithProviderInfo(peerID, addrs),
115115
WithIdentity(identity),
116-
WithUserAgent(testUserAgent),
117116
WithHTTPClient(recordingHTTPClient),
118117
}
119118
c, err := New(serverAddr, append(defaultClientOpts, clientsOpts...)...)

0 commit comments

Comments
 (0)