Skip to content

Commit c69b48d

Browse files
authored
Merge pull request #116 from deploymenttheory/dev
Added configuration options for setting up redirect handler at time of client build
2 parents 429b9ad + f01812d commit c69b48d

File tree

6 files changed

+182
-112
lines changed

6 files changed

+182
-112
lines changed

httpclient/httpclient_client.go

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ package httpclient
99

1010
import (
1111
"net/http"
12-
"net/http/cookiejar"
1312
"sync"
1413
"time"
1514

1615
"github.com/deploymenttheory/go-api-http-client/logger"
16+
"github.com/deploymenttheory/go-api-http-client/redirecthandler"
1717
"go.uber.org/zap"
1818
)
1919

@@ -65,6 +65,8 @@ type ClientOptions struct {
6565
MaxRetryAttempts int // Config item defines the max number of retry request attempts for retryable HTTP methods.
6666
EnableDynamicRateLimiting bool // Field for defining whether dynamic rate limiting should be enabled.
6767
MaxConcurrentRequests int // Field for defining the maximum number of concurrent requests allowed in the semaphore
68+
FollowRedirects bool // Flag to enable/disable following redirects
69+
MaxRedirects int // Maximum number of redirects to follow
6870
TokenRefreshBufferPeriod time.Duration
6971
TotalRetryDuration time.Duration
7072
CustomTimeout time.Duration
@@ -99,29 +101,31 @@ func BuildClient(config ClientConfig) (*Client, error) {
99101
return nil, log.Error("Failed to load API handler", zap.String("APIType", config.Environment.APIType), zap.Error(err))
100102
}
101103

104+
// Determine the authentication method using the helper function
105+
authMethod, err := DetermineAuthMethod(config.Auth)
106+
if err != nil {
107+
log.Error("Failed to determine authentication method", zap.Error(err))
108+
return nil, err
109+
}
110+
102111
log.Info("Initializing new HTTP client with the provided configuration")
103112

104113
// Initialize the internal HTTP client
105114
httpClient := &http.Client{
106115
Timeout: config.ClientOptions.CustomTimeout,
107116
}
108117

109-
// Conditionally create and set a cookie jar if the option is enabled
110-
if config.ClientOptions.EnableCookieJar {
111-
jar, err := cookiejar.New(nil) // nil means no options, which uses default options
112-
if err != nil {
113-
return nil, log.Error("Failed to create cookie jar", zap.Error(err))
114-
}
115-
httpClient.Jar = jar
118+
// Conditionally setup cookie jar
119+
if err := setupCookieJar(httpClient, config.ClientOptions.EnableCookieJar, log); err != nil {
120+
log.Error("Error setting up cookie jar", zap.Error(err))
121+
return nil, err
116122
}
117123

118-
// Determine the authentication method using the helper function
119-
authMethod, err := DetermineAuthMethod(config.Auth)
120-
if err != nil {
121-
log.Error("Failed to determine authentication method", zap.Error(err))
124+
// Conditionally setup redirect handling
125+
if err := redirecthandler.SetupRedirectHandler(httpClient, config.ClientOptions.FollowRedirects, config.ClientOptions.MaxRedirects, log); err != nil {
126+
log.Error("Failed to set up redirect handler", zap.Error(err))
122127
return nil, err
123128
}
124-
125129
// Create a new HTTP client with the provided configuration.
126130
client := &Client{
127131
APIHandler: apiHandler,
@@ -148,6 +152,8 @@ func BuildClient(config ClientConfig) (*Client, error) {
148152
zap.Bool("Cookie Jar Enabled", config.ClientOptions.EnableCookieJar),
149153
zap.Int("Max Retry Attempts", config.ClientOptions.MaxRetryAttempts),
150154
zap.Int("Max Concurrent Requests", config.ClientOptions.MaxConcurrentRequests),
155+
zap.Bool("Follow Redirects", config.ClientOptions.FollowRedirects),
156+
zap.Int("Max Redirects", config.ClientOptions.MaxRedirects),
151157
zap.Bool("Enable Dynamic Rate Limiting", config.ClientOptions.EnableDynamicRateLimiting),
152158
zap.Duration("Token Refresh Buffer Period", config.ClientOptions.TokenRefreshBufferPeriod),
153159
zap.Duration("Total Retry Duration", config.ClientOptions.TotalRetryDuration),

httpclient/httpclient_client_configuration.go

Lines changed: 60 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ const (
2020
DefaultTokenBufferPeriod = 5 * time.Minute
2121
DefaultTotalRetryDuration = 5 * time.Minute
2222
DefaultTimeout = 10 * time.Second
23+
FollowRedirects = true
24+
MaxRedirects = 10
2325
)
2426

2527
// LoadConfigFromFile loads configuration values from a JSON file into the ClientConfig struct.
@@ -121,6 +123,13 @@ func LoadConfigFromEnv(config *ClientConfig) (*ClientConfig, error) {
121123
config.ClientOptions.CustomTimeout = parseDuration(getEnvOrDefault("CUSTOM_TIMEOUT", config.ClientOptions.CustomTimeout.String()), DefaultTimeout)
122124
log.Printf("CustomTimeout env value found and set to: %s", config.ClientOptions.CustomTimeout)
123125

126+
// Redirects
127+
config.ClientOptions.FollowRedirects = parseBool(getEnvOrDefault("FOLLOW_REDIRECTS", strconv.FormatBool(config.ClientOptions.FollowRedirects)))
128+
log.Printf("FollowRedirects env value set to: %t", config.ClientOptions.FollowRedirects)
129+
130+
config.ClientOptions.MaxRedirects = parseInt(getEnvOrDefault("MAX_REDIRECTS", strconv.Itoa(config.ClientOptions.MaxRedirects)), MaxRedirects)
131+
log.Printf("MaxRedirects env value set to: %d", config.ClientOptions.MaxRedirects)
132+
124133
// Set default values if necessary
125134
setLoggerDefaultValues(config)
126135
setClientDefaultValues(config)
@@ -133,41 +142,6 @@ func LoadConfigFromEnv(config *ClientConfig) (*ClientConfig, error) {
133142
return config, nil
134143
}
135144

136-
// Helper function to get environment variable or default value
137-
func getEnvOrDefault(envKey string, defaultValue string) string {
138-
if value, exists := os.LookupEnv(envKey); exists {
139-
return value
140-
}
141-
return defaultValue
142-
}
143-
144-
// Helper function to parse boolean from environment variable
145-
func parseBool(value string) bool {
146-
result, err := strconv.ParseBool(value)
147-
if err != nil {
148-
return false
149-
}
150-
return result
151-
}
152-
153-
// Helper function to parse int from environment variable
154-
func parseInt(value string, defaultVal int) int {
155-
result, err := strconv.Atoi(value)
156-
if err != nil {
157-
return defaultVal
158-
}
159-
return result
160-
}
161-
162-
// Helper function to parse duration from environment variable
163-
func parseDuration(value string, defaultVal time.Duration) time.Duration {
164-
result, err := time.ParseDuration(value)
165-
if err != nil {
166-
return defaultVal
167-
}
168-
return result
169-
}
170-
171145
// validateMandatoryConfiguration checks if any essential configuration fields are missing,
172146
// and returns an error with details about the missing configurations.
173147
// This ensures the caller can understand what specific configurations need attention.
@@ -212,6 +186,12 @@ func validateMandatoryConfiguration(config *ClientConfig) error {
212186
}
213187
}
214188

189+
// Default setting for MaxRedirects
190+
if config.ClientOptions.MaxRedirects <= 0 {
191+
config.ClientOptions.MaxRedirects = MaxRedirects
192+
log.Printf("MaxRedirects not set or invalid, set to default value: %d", MaxRedirects)
193+
}
194+
215195
// If there are missing fields, construct and return an error message detailing what is missing
216196
if len(missingFields) > 0 {
217197
errorMessage := fmt.Sprintf("Mandatory configuration missing: %s. Ensure that either OAuth credentials (ClientID and ClientSecret) or Basic Auth credentials (Username and Password) are fully provided.", strings.Join(missingFields, ", "))
@@ -263,10 +243,55 @@ func setClientDefaultValues(config *ClientConfig) {
263243
log.Printf("CustomTimeout not set, set to default value: %s", DefaultTimeout)
264244
}
265245

246+
if !config.ClientOptions.FollowRedirects {
247+
config.ClientOptions.FollowRedirects = FollowRedirects
248+
log.Printf("FollowRedirects not set, set to default value: %t", FollowRedirects)
249+
}
250+
251+
if config.ClientOptions.MaxRedirects <= 0 {
252+
config.ClientOptions.MaxRedirects = MaxRedirects
253+
log.Printf("MaxRedirects not set or invalid, set to default value: %d", MaxRedirects)
254+
}
255+
266256
// Log completion of setting default values
267257
log.Println("Default values set for client configuration")
268258
}
269259

260+
// Helper function to get environment variable or default value
261+
func getEnvOrDefault(envKey string, defaultValue string) string {
262+
if value, exists := os.LookupEnv(envKey); exists {
263+
return value
264+
}
265+
return defaultValue
266+
}
267+
268+
// Helper function to parse boolean from environment variable
269+
func parseBool(value string) bool {
270+
result, err := strconv.ParseBool(value)
271+
if err != nil {
272+
return false
273+
}
274+
return result
275+
}
276+
277+
// Helper function to parse int from environment variable
278+
func parseInt(value string, defaultVal int) int {
279+
result, err := strconv.Atoi(value)
280+
if err != nil {
281+
return defaultVal
282+
}
283+
return result
284+
}
285+
286+
// Helper function to parse duration from environment variable
287+
func parseDuration(value string, defaultVal time.Duration) time.Duration {
288+
result, err := time.ParseDuration(value)
289+
if err != nil {
290+
return defaultVal
291+
}
292+
return result
293+
}
294+
270295
// setLoggerDefaultValues sets default values for the client logger configuration options if none are provided.
271296
// It checks each configuration option and sets it to the default value if it is either negative, zero,
272297
// or not set. It also logs each default value being set.

httpclient/httpclient_cookies.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,28 @@
22
package httpclient
33

44
import (
5+
"fmt"
56
"net/http"
7+
"net/http/cookiejar"
68
"strings"
9+
10+
"github.com/deploymenttheory/go-api-http-client/logger"
11+
"go.uber.org/zap"
712
)
813

14+
// setupCookieJar initializes the HTTP client with a cookie jar if enabled in the configuration.
15+
func setupCookieJar(client *http.Client, enableCookieJar bool, log logger.Logger) error {
16+
if enableCookieJar {
17+
jar, err := cookiejar.New(nil) // nil options use default options
18+
if err != nil {
19+
log.Error("Failed to create cookie jar", zap.Error(err))
20+
return fmt.Errorf("setupCookieJar failed: %w", err) // Wrap and return the error
21+
}
22+
client.Jar = jar
23+
}
24+
return nil
25+
}
26+
927
// RedactSensitiveCookies redacts sensitive information from cookies.
1028
// It takes a slice of *http.Cookie and returns a redacted slice of *http.Cookie.
1129
func RedactSensitiveCookies(cookies []*http.Cookie) []*http.Cookie {

httpclient/httpclient_request.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"time"
99

1010
"github.com/deploymenttheory/go-api-http-client/logger"
11-
"github.com/deploymenttheory/go-api-http-client/redirecthandler"
1211
"github.com/deploymenttheory/go-api-http-client/status"
1312
"github.com/google/uuid"
1413
"go.uber.org/zap"
@@ -64,10 +63,6 @@ import (
6463
func (c *Client) DoRequest(method, endpoint string, body, out interface{}) (*http.Response, error) {
6564
log := c.Logger
6665

67-
// Apply redirect handling to the client with MaxRedirects set to 10
68-
redirectHandler := redirecthandler.NewRedirectHandler(log, 10)
69-
redirectHandler.WithRedirectHandling(c.httpClient)
70-
7166
if IsIdempotentHTTPMethod(method) {
7267
return c.executeRequestWithRetries(method, endpoint, body, out)
7368
} else if IsNonIdempotentHTTPMethod(method) {

redirecthandler/redirecthandler.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,9 @@ func (r *RedirectHandler) WithRedirectHandling(client *http.Client) {
4747

4848
// checkRedirect implements the redirect handling logic.
4949
func (r *RedirectHandler) checkRedirect(req *http.Request, via []*http.Request) error {
50-
defer r.clearRedirectHistory(req) // Ensure redirect history is always cleared to prevent memory leaks
50+
51+
// Ensure redirect history is always cleared to prevent memory leaks
52+
defer r.clearRedirectHistory(req)
5153

5254
// Non-idempotent methods handling
5355
if req.Method == http.MethodPost || req.Method == http.MethodPatch {
@@ -215,3 +217,18 @@ func (r *RedirectHandler) GetRedirectHistory(req *http.Request) []*url.URL {
215217

216218
return r.RedirectHistories[req]
217219
}
220+
221+
// SetupRedirectHandler configures the HTTP client for redirect handling based on the client configuration.
222+
func SetupRedirectHandler(client *http.Client, followRedirects bool, maxRedirects int, log logger.Logger) error {
223+
if followRedirects {
224+
if maxRedirects < 0 {
225+
log.Error("Invalid maxRedirects value", zap.Int("maxRedirects", maxRedirects))
226+
return fmt.Errorf("invalid maxRedirects value: %d", maxRedirects)
227+
}
228+
229+
redirectHandler := NewRedirectHandler(log, maxRedirects)
230+
redirectHandler.WithRedirectHandling(client)
231+
log.Info("Redirect handling enabled", zap.Int("MaxRedirects", maxRedirects))
232+
}
233+
return nil
234+
}

0 commit comments

Comments
 (0)