Skip to content

Commit 94fb2ef

Browse files
committed
fix: Prevent concurrent reads and writes of client header map
The current implementation resulted in a `concurrent map iteration and map write` error in the Dynatrace Terraform Provider, as `(c *Client) SetHeader(...)` could be called while the map was being read over in `(c *Client) sendWithRetries(...)`. Now the map is protected with a `sync.RWMutex` to prevent this. Furthermore, as the headers should be static between retries, request headers are now set in `(c *Client) acquireLockAndSendWithRetries(...)` rather than `(c *Client) sendWithRetries(...)`. This is important as `sync.RWMutex` should not be reentered by the same reader or writer.
1 parent 4a52a35 commit 94fb2ef

File tree

1 file changed

+22
-13
lines changed

1 file changed

+22
-13
lines changed

api/rest/client.go

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"io"
2323
"net/http"
2424
"net/url"
25+
"sync"
2526
"time"
2627

2728
"github.com/go-logr/logr"
@@ -99,9 +100,10 @@ func WithRateLimiter() Option {
99100

100101
// Client represents a general HTTP client
101102
type Client struct {
102-
baseURL *url.URL // Base URL of the server
103-
httpClient *http.Client // Custom HTTP client support
104-
headers map[string]string // Custom headers to be set
103+
baseURL *url.URL // Base URL of the server
104+
httpClient *http.Client // Custom HTTP client support
105+
headers map[string]string // Custom headers to be set
106+
headerMutex *sync.RWMutex
105107

106108
concurrentRequestLimiter *ConcurrentRequestLimiter // Concurrent request limiter component (optional)
107109
timeout time.Duration // Request timeout (optional)
@@ -113,9 +115,10 @@ type Client struct {
113115
// NewClient creates a new instance of the Client with specified options.
114116
func NewClient(baseURL *url.URL, httpClient *http.Client, opts ...Option) *Client {
115117
client := &Client{
116-
baseURL: baseURL,
117-
headers: make(map[string]string),
118-
httpClient: httpClient,
118+
baseURL: baseURL,
119+
headers: make(map[string]string),
120+
headerMutex: &sync.RWMutex{},
121+
httpClient: httpClient,
119122
}
120123

121124
for _, opt := range opts {
@@ -162,6 +165,8 @@ func (c *Client) DELETE(ctx context.Context, endpoint string, options RequestOpt
162165

163166
// SetHeader sets a custom header for the HTTP client.
164167
func (c *Client) SetHeader(key, value string) {
168+
c.headerMutex.Lock()
169+
defer c.headerMutex.Unlock()
165170
c.headers[key] = value
166171
}
167172

@@ -171,22 +176,17 @@ func (c *Client) BaseURL() *url.URL {
171176
}
172177

173178
func (c *Client) acquireLockAndSendWithRetries(ctx context.Context, req *http.Request, retryCount int, options RequestOptions) (*http.Response, error) {
174-
175179
// Apply concurrent request limiting if concurrentRequestLimiter is set
176180
if c.concurrentRequestLimiter != nil {
177181
c.concurrentRequestLimiter.Acquire()
178182
defer c.concurrentRequestLimiter.Release()
179183
}
180184

185+
c.setHeadersOnRequest(req, options)
181186
return c.sendWithRetries(ctx, req, retryCount, options)
182187
}
183188

184-
func (c *Client) sendWithRetries(ctx context.Context, req *http.Request, retryCount int, options RequestOptions) (*http.Response, error) {
185-
logger := logr.FromContextOrDiscard(ctx)
186-
if c.rateLimiter != nil {
187-
c.rateLimiter.Wait(ctx) // If a limit is reached, this blocks until operations are permitted again
188-
}
189-
189+
func (c *Client) setHeadersOnRequest(req *http.Request, options RequestOptions) {
190190
// Set Content-Type header accordingly
191191
if options.ContentType != "" {
192192
req.Header.Set("Content-type", options.ContentType)
@@ -195,9 +195,18 @@ func (c *Client) sendWithRetries(ctx context.Context, req *http.Request, retryCo
195195
}
196196

197197
// set fixed headers
198+
c.headerMutex.RLock()
199+
defer c.headerMutex.RUnlock()
198200
for key, value := range c.headers {
199201
req.Header.Set(key, value)
200202
}
203+
}
204+
205+
func (c *Client) sendWithRetries(ctx context.Context, req *http.Request, retryCount int, options RequestOptions) (*http.Response, error) {
206+
logger := logr.FromContextOrDiscard(ctx)
207+
if c.rateLimiter != nil {
208+
c.rateLimiter.Wait(ctx) // If a limit is reached, this blocks until operations are permitted again
209+
}
201210

202211
if c.httpClient == nil {
203212
c.httpClient = &http.Client{

0 commit comments

Comments
 (0)