feat: allow creating own http.RoundTripper for http.Client with HttpRoundTripperProvider when customizing pre-configured http.Transport with HttpTransportCustomizer is not sufficient (#31)

feat: allow creating own http.RoundTripper for http.Client with `HttpRoundTripperProvider` when customizing pre-configured http.Transport with `HttpTransportCustomizer` is not sufficient
This commit is contained in:
Mateusz Filipowicz
2025-02-19 01:28:11 +01:00
committed by GitHub
parent 7c7ef98c03
commit d3a3d5a342
2 changed files with 55 additions and 21 deletions

View File

@@ -58,7 +58,17 @@ if err != nil {
## Customizing the HTTP Client ## Customizing the HTTP Client
You can provide your own HTTP client configuration using the `HttpTransportCustomizer` callback. This is useful if you need to tweak connection settings like timeouts, idle connection settings, or TLS configurations: There are two ways to customize the HTTP client used by the UniFi client:
1. Using the `HttpTransportCustomizer`.
2. Using the `HttpRoundTripperProvider`.
Those methods are mutually exclusive, and only one can be used at a time. If both are provided, the `HttpRoundTripperProvider` takes precedence,
unless it returns `nil`, in which case the `HttpTransportCustomizer` is used if defined (or default transport is used).
### Using `HttpTransportCustomizer`
You can provide your own HTTP client transport configuration using the `HttpTransportCustomizer` callback. This is useful if you need to tweak connection settings like timeouts, idle connection settings,
or TLS configurations:
```go ```go
c, err := unifi.NewClient(&unifi.ClientConfig{ c, err := unifi.NewClient(&unifi.ClientConfig{
@@ -76,6 +86,21 @@ if err != nil {
} }
``` ```
### Using `HttpRoundTripperProvider`
You can provide your own HTTP client configuration using the `HttpRoundTripperProvider` callback. This is useful if you need to create a custom round tripper, when `http.Transport` is not enough:
```go
c, err := unifi.NewClient(&unifi.ClientConfig{
BaseURL: "https://unifi.localdomain",
APIKey: "your-api-key",
HttpRoundTripperProvider: func() http.RoundTripper {
// Create a custom HTTP Round Tripper instance
return &http.Transport{}, nil
},
})
```
## Using Interceptors ## Using Interceptors
Interceptors let you hook into the request and response flow. They can be used for logging, metrics, or modifying requests/responses. Interceptors let you hook into the request and response flow. They can be used for logging, metrics, or modifying requests/responses.

View File

@@ -53,25 +53,27 @@ Fields:
Timeout: The maximum duration to wait for responses; default is no timeout. Timeout: The maximum duration to wait for responses; default is no timeout.
VerifySSL: When false, disables SSL certificate verification. VerifySSL: When false, disables SSL certificate verification.
Interceptors: A slice of ClientInterceptor implementations that can modify requests and responses. Interceptors: A slice of ClientInterceptor implementations that can modify requests and responses.
HttpTransportCustomizer:An optional function to customize the HTTP transport (e.g., for custom TLS settings). HttpTransportCustomizer: An optional function to customize the HTTP transport (e.g., for custom TLS settings).
HttpRoundTripperProvider: A function that returns a http.RoundTripper for customizing the HTTP client. If both HttpTransportCustomizer and HttpRoundTripperProvider are provided, HttpRoundTripperProvider takes precedence.
UserAgent: The User-Agent header string for outgoing HTTP requests. UserAgent: The User-Agent header string for outgoing HTTP requests.
ErrorHandler: A custom handler for processing HTTP response errors. ErrorHandler: A custom handler for processing HTTP response errors.
UseLocking: If true, enables internal locking for concurrent request processing. UseLocking: If true, enables internal locking for concurrent request processing.
ValidationMode:The mode for validating request bodies. Can be "soft", "hard", or "disable". ValidationMode:The mode for validating request bodies. Can be "soft", "hard", or "disable".
*/ */
type ClientConfig struct { type ClientConfig struct {
URL string `validate:"required,http_url"` URL string `validate:"required,http_url"`
APIKey string `validate:"required_without_all=User Password"` APIKey string `validate:"required_without_all=User Password"`
User string `validate:"excluded_with=APIKey,required_with=Password"` User string `validate:"excluded_with=APIKey,required_with=Password"`
Password string `validate:"excluded_with=APIKey,required_with=User"` Password string `validate:"excluded_with=APIKey,required_with=User"`
Timeout time.Duration // How long to wait for replies, default: forever. Timeout time.Duration // How long to wait for replies, default: forever.
VerifySSL bool VerifySSL bool
Interceptors []ClientInterceptor Interceptors []ClientInterceptor
HttpTransportCustomizer HttpTransportCustomizer HttpTransportCustomizer HttpTransportCustomizer
UserAgent string HttpRoundTripperProvider func() http.RoundTripper
ErrorHandler ResponseErrorHandler UserAgent string
UseLocking bool ErrorHandler ResponseErrorHandler
ValidationMode validationMode `validate:"omitempty,oneof=soft hard disable"` UseLocking bool
ValidationMode validationMode `validate:"omitempty,oneof=soft hard disable"`
} }
// Credentials abstracts authentication credentials. // Credentials abstracts authentication credentials.
@@ -152,20 +154,27 @@ func parseBaseURL(base string) (*url.URL, error) {
} }
func newClientFromConfig(config *ClientConfig, v *validator) (*client, error) { func newClientFromConfig(config *ClientConfig, v *validator) (*client, error) {
var rt http.RoundTripper
var err error var err error
config.URL = strings.TrimRight(config.URL, "/") config.URL = strings.TrimRight(config.URL, "/")
transport := &http.Transport{ if config.HttpRoundTripperProvider != nil {
Proxy: http.ProxyFromEnvironment, rt = config.HttpRoundTripperProvider()
TLSClientConfig: &tls.Config{InsecureSkipVerify: !config.VerifySSL},
} }
if config.HttpTransportCustomizer != nil { if rt == nil {
if transport, err = config.HttpTransportCustomizer(transport); err != nil { transport := &http.Transport{
return nil, fmt.Errorf("failed customizing HTTP transport: %w", err) Proxy: http.ProxyFromEnvironment,
TLSClientConfig: &tls.Config{InsecureSkipVerify: !config.VerifySSL},
} }
if config.HttpTransportCustomizer != nil {
if transport, err = config.HttpTransportCustomizer(transport); err != nil {
return nil, fmt.Errorf("failed customizing HTTP transport: %w", err)
}
}
rt = transport
} }
httpClient := &http.Client{ httpClient := &http.Client{
Timeout: config.Timeout, Timeout: config.Timeout,
Transport: transport, Transport: rt,
} }
if config.APIKey == "" { if config.APIKey == "" {
jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List}) jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})