Client Configuration

8 minute read

HTTP Client Configuration

Most Terraform providers communicate with APIs over HTTP. Proper client configuration is essential for reliability, performance, and user experience.

In this chapter, you’ll learn how to configure HTTP clients with timeouts, retries, custom headers, and more.

Basic HTTP Client

Simple Client

func (p *MyCloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
    // Basic HTTP client
    httpClient := &http.Client{
        Timeout: 30 * time.Second,
    }
    
    client := api.NewClient(httpClient)
    resp.ResourceData = client
}

With Custom Transport

func (p *MyCloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
    transport := &http.Transport{
        MaxIdleConns:        100,
        MaxIdleConnsPerHost: 10,
        IdleConnTimeout:     90 * time.Second,
    }
    
    httpClient := &http.Client{
        Timeout:   30 * time.Second,
        Transport: transport,
    }
    
    client := api.NewClient(httpClient)
    resp.ResourceData = client
}

Timeout Configuration

Schema

resp.Schema = schema.Schema{
    Attributes: map[string]schema.Attribute{
        "timeout": schema.NumberAttribute{
            Optional:            true,
            MarkdownDescription: "Request timeout in seconds. Defaults to 30.",
            Validators: []validator.Number{
                numbervalidator.Between(1, 300),
            },
        },
        
        "connect_timeout": schema.NumberAttribute{
            Optional:            true,
            MarkdownDescription: "Connection timeout in seconds. Defaults to 10.",
            Validators: []validator.Number{
                numbervalidator.Between(1, 60),
            },
        },
        
        "read_timeout": schema.NumberAttribute{
            Optional:            true,
            MarkdownDescription: "Read timeout in seconds. Defaults to 30.",
            Validators: []validator.Number{
                numbervalidator.Between(1, 300),
            },
        },
    },
}

Implementation

func (p *MyCloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
    var config MyCloudProviderModel
    req.Config.Get(ctx, &config)
    
    // Parse timeouts with defaults
    timeout := 30 * time.Second
    if !config.Timeout.IsNull() {
        timeoutFloat, _ := config.Timeout.ValueBigFloat().Int64()
        timeout = time.Duration(timeoutFloat) * time.Second
    }
    
    connectTimeout := 10 * time.Second
    if !config.ConnectTimeout.IsNull() {
        timeoutFloat, _ := config.ConnectTimeout.ValueBigFloat().Int64()
        connectTimeout = time.Duration(timeoutFloat) * time.Second
    }
    
    readTimeout := 30 * time.Second
    if !config.ReadTimeout.IsNull() {
        timeoutFloat, _ := config.ReadTimeout.ValueBigFloat().Int64()
        readTimeout = time.Duration(timeoutFloat) * time.Second
    }
    
    // Configure transport with timeouts
    transport := &http.Transport{
        DialContext: (&net.Dialer{
            Timeout: connectTimeout,
        }).DialContext,
        ResponseHeaderTimeout: readTimeout,
    }
    
    httpClient := &http.Client{
        Timeout:   timeout,
        Transport: transport,
    }
    
    client := api.NewClient(httpClient)
    resp.ResourceData = client
}

Usage

provider "mycloud" {
  api_key         = var.api_key
  timeout         = 60    # Overall request timeout
  connect_timeout = 15    # Connection timeout
  read_timeout    = 45    # Read timeout
}

Retry Logic

Schema

resp.Schema = schema.Schema{
    Attributes: map[string]schema.Attribute{
        "max_retries": schema.NumberAttribute{
            Optional:            true,
            MarkdownDescription: "Maximum number of retry attempts. Defaults to 3.",
            Validators: []validator.Number{
                numbervalidator.Between(0, 10),
            },
        },
        
        "retry_delay": schema.NumberAttribute{
            Optional:            true,
            MarkdownDescription: "Initial retry delay in seconds. Defaults to 1. Uses exponential backoff.",
            Validators: []validator.Number{
                numbervalidator.Between(1, 30),
            },
        },
    },
}

Implementation with Retry Transport

type retryTransport struct {
    Transport  http.RoundTripper
    MaxRetries int
    RetryDelay time.Duration
}

func (t *retryTransport) RoundTrip(req *http.Request) (*http.Response, error) {
    var resp *http.Response
    var err error
    
    for attempt := 0; attempt <= t.MaxRetries; attempt++ {
        // Clone request for retry
        clonedReq := req.Clone(req.Context())
        
        // Attempt request
        resp, err = t.Transport.RoundTrip(clonedReq)
        
        // Check if we should retry
        if err == nil && !shouldRetry(resp.StatusCode) {
            return resp, nil
        }
        
        // Don't retry on last attempt
        if attempt == t.MaxRetries {
            break
        }
        
        // Exponential backoff
        delay := t.RetryDelay * time.Duration(1<<uint(attempt))
        time.Sleep(delay)
    }
    
    return resp, err
}

func shouldRetry(statusCode int) bool {
    // Retry on server errors and rate limits
    return statusCode >= 500 || statusCode == 429
}

Configure Method

func (p *MyCloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
    var config MyCloudProviderModel
    req.Config.Get(ctx, &config)
    
    // Parse retry config
    maxRetries := 3
    if !config.MaxRetries.IsNull() {
        retriesFloat, _ := config.MaxRetries.ValueBigFloat().Int64()
        maxRetries = int(retriesFloat)
    }
    
    retryDelay := 1 * time.Second
    if !config.RetryDelay.IsNull() {
        delayFloat, _ := config.RetryDelay.ValueBigFloat().Int64()
        retryDelay = time.Duration(delayFloat) * time.Second
    }
    
    // Create client with retry logic
    baseTransport := http.DefaultTransport
    
    transport := &retryTransport{
        Transport:  baseTransport,
        MaxRetries: maxRetries,
        RetryDelay: retryDelay,
    }
    
    httpClient := &http.Client{
        Timeout:   30 * time.Second,
        Transport: transport,
    }
    
    client := api.NewClient(httpClient)
    resp.ResourceData = client
}

Usage

provider "mycloud" {
  api_key     = var.api_key
  max_retries = 5
  retry_delay = 2  # seconds
}

Custom Headers

Schema

resp.Schema = schema.Schema{
    Attributes: map[string]schema.Attribute{
        "user_agent": schema.StringAttribute{
            Optional:            true,
            MarkdownDescription: "Custom User-Agent header",
        },
        
        "extra_headers": schema.MapAttribute{
            ElementType:         types.StringType,
            Optional:            true,
            MarkdownDescription: "Additional HTTP headers to include in all requests",
        },
    },
}

Implementation

type headerTransport struct {
    Transport    http.RoundTripper
    UserAgent    string
    ExtraHeaders map[string]string
}

func (t *headerTransport) RoundTrip(req *http.Request) (*http.Response, error) {
    // Add user agent
    if t.UserAgent != "" {
        req.Header.Set("User-Agent", t.UserAgent)
    }
    
    // Add extra headers
    for key, value := range t.ExtraHeaders {
        req.Header.Set(key, value)
    }
    
    return t.Transport.RoundTrip(req)
}

func (p *MyCloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
    var config MyCloudProviderModel
    req.Config.Get(ctx, &config)
    
    // Parse extra headers
    extraHeaders := make(map[string]string)
    if !config.ExtraHeaders.IsNull() {
        config.ExtraHeaders.ElementsAs(ctx, &extraHeaders, false)
    }
    
    // Default user agent
    userAgent := fmt.Sprintf("terraform-provider-mycloud/%s", p.version)
    if !config.UserAgent.IsNull() {
        userAgent = config.UserAgent.ValueString()
    }
    
    transport := &headerTransport{
        Transport:    http.DefaultTransport,
        UserAgent:    userAgent,
        ExtraHeaders: extraHeaders,
    }
    
    httpClient := &http.Client{
        Timeout:   30 * time.Second,
        Transport: transport,
    }
    
    client := api.NewClient(httpClient)
    resp.ResourceData = client
}

Usage

provider "mycloud" {
  api_key    = var.api_key
  user_agent = "MyCustomAgent/1.0"
  
  extra_headers = {
    "X-Custom-Header" = "value"
    "X-Request-ID"    = "12345"
  }
}

Connection Pooling

Schema

resp.Schema = schema.Schema{
    Attributes: map[string]schema.Attribute{
        "max_connections": schema.NumberAttribute{
            Optional:            true,
            MarkdownDescription: "Maximum number of idle connections. Defaults to 100.",
        },
        
        "max_connections_per_host": schema.NumberAttribute{
            Optional:            true,
            MarkdownDescription: "Maximum idle connections per host. Defaults to 10.",
        },
        
        "idle_connection_timeout": schema.NumberAttribute{
            Optional:            true,
            MarkdownDescription: "Idle connection timeout in seconds. Defaults to 90.",
        },
    },
}

Implementation

func (p *MyCloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
    var config MyCloudProviderModel
    req.Config.Get(ctx, &config)
    
    // Parse connection pool settings
    maxConns := 100
    if !config.MaxConnections.IsNull() {
        connsFloat, _ := config.MaxConnections.ValueBigFloat().Int64()
        maxConns = int(connsFloat)
    }
    
    maxConnsPerHost := 10
    if !config.MaxConnectionsPerHost.IsNull() {
        connsFloat, _ := config.MaxConnectionsPerHost.ValueBigFloat().Int64()
        maxConnsPerHost = int(connsFloat)
    }
    
    idleTimeout := 90 * time.Second
    if !config.IdleConnectionTimeout.IsNull() {
        timeoutFloat, _ := config.IdleConnectionTimeout.ValueBigFloat().Int64()
        idleTimeout = time.Duration(timeoutFloat) * time.Second
    }
    
    transport := &http.Transport{
        MaxIdleConns:        maxConns,
        MaxIdleConnsPerHost: maxConnsPerHost,
        IdleConnTimeout:     idleTimeout,
        MaxConnsPerHost:     maxConnsPerHost * 2,
    }
    
    httpClient := &http.Client{
        Timeout:   30 * time.Second,
        Transport: transport,
    }
    
    client := api.NewClient(httpClient)
    resp.ResourceData = client
}

TLS Configuration

Schema

resp.Schema = schema.Schema{
    Attributes: map[string]schema.Attribute{
        "insecure_skip_verify": schema.BoolAttribute{
            Optional:            true,
            MarkdownDescription: "Skip TLS certificate verification. Not recommended for production.",
        },
        
        "ca_cert": schema.StringAttribute{
            Optional:            true,
            MarkdownDescription: "Path to CA certificate file",
        },
        
        "min_tls_version": schema.StringAttribute{
            Optional:            true,
            MarkdownDescription: "Minimum TLS version: '1.0', '1.1', '1.2', '1.3'. Defaults to '1.2'.",
            Validators: []validator.String{
                stringvalidator.OneOf("1.0", "1.1", "1.2", "1.3"),
            },
        },
    },
}

Implementation

func (p *MyCloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
    var config MyCloudProviderModel
    req.Config.Get(ctx, &config)
    
    // TLS configuration
    tlsConfig := &tls.Config{}
    
    // Skip verify (for dev/testing only)
    if !config.InsecureSkipVerify.IsNull() {
        tlsConfig.InsecureSkipVerify = config.InsecureSkipVerify.ValueBool()
        
        if tlsConfig.InsecureSkipVerify {
            tflog.Warn(ctx, "TLS certificate verification is disabled. This is not recommended for production.")
        }
    }
    
    // CA certificate
    if !config.CACert.IsNull() {
        caCertPath := config.CACert.ValueString()
        caCert, err := os.ReadFile(caCertPath)
        if err != nil {
            resp.Diagnostics.AddError(
                "Failed to Load CA Certificate",
                fmt.Sprintf("Could not read CA certificate: %s", err.Error()),
            )
            return
        }
        
        caCertPool := x509.NewCertPool()
        if !caCertPool.AppendCertsFromPEM(caCert) {
            resp.Diagnostics.AddError(
                "Invalid CA Certificate",
                "Could not parse CA certificate",
            )
            return
        }
        
        tlsConfig.RootCAs = caCertPool
    }
    
    // Minimum TLS version
    minTLSVersion := config.MinTLSVersion.ValueString()
    switch minTLSVersion {
    case "1.0":
        tlsConfig.MinVersion = tls.VersionTLS10
    case "1.1":
        tlsConfig.MinVersion = tls.VersionTLS11
    case "1.3":
        tlsConfig.MinVersion = tls.VersionTLS13
    default: // "1.2" or empty
        tlsConfig.MinVersion = tls.VersionTLS12
    }
    
    transport := &http.Transport{
        TLSClientConfig: tlsConfig,
    }
    
    httpClient := &http.Client{
        Timeout:   30 * time.Second,
        Transport: transport,
    }
    
    client := api.NewClient(httpClient)
    resp.ResourceData = client
}

Usage

provider "mycloud" {
  api_key           = var.api_key
  ca_cert           = "/path/to/ca.crt"
  min_tls_version   = "1.2"
  # insecure_skip_verify = true  # Don't use in production!
}

Complete Client Configuration

Comprehensive Example

func (p *MyCloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
    var config MyCloudProviderModel
    diags := req.Config.Get(ctx, &config)
    resp.Diagnostics.Append(diags...)
    if resp.Diagnostics.HasError() {
        return
    }
    
    // Get API key
    apiKey := config.APIKey.ValueString()
    if apiKey == "" {
        apiKey = os.Getenv("MYCLOUD_API_KEY")
    }
    if apiKey == "" {
        resp.Diagnostics.AddError("Missing API Key", "...")
        return
    }
    
    // Parse timeouts
    timeout := parseTimeout(config.Timeout, 30)
    connectTimeout := parseTimeout(config.ConnectTimeout, 10)
    readTimeout := parseTimeout(config.ReadTimeout, 30)
    
    // Parse retry config
    maxRetries := parseInt(config.MaxRetries, 3)
    retryDelay := parseTimeout(config.RetryDelay, 1)
    
    // Parse connection pool config
    maxConns := parseInt(config.MaxConnections, 100)
    maxConnsPerHost := parseInt(config.MaxConnectionsPerHost, 10)
    idleTimeout := parseTimeout(config.IdleConnectionTimeout, 90)
    
    // TLS configuration
    tlsConfig := &tls.Config{
        MinVersion: tls.VersionTLS12,
    }
    
    // Build transport
    transport := &http.Transport{
        DialContext: (&net.Dialer{
            Timeout: connectTimeout,
        }).DialContext,
        ResponseHeaderTimeout: readTimeout,
        MaxIdleConns:          maxConns,
        MaxIdleConnsPerHost:   maxConnsPerHost,
        IdleConnTimeout:       idleTimeout,
        TLSClientConfig:       tlsConfig,
    }
    
    // Add authentication
    transport = &apiKeyTransport{
        APIKey:    apiKey,
        Transport: transport,
    }
    
    // Add retry logic
    transport = &retryTransport{
        Transport:  transport,
        MaxRetries: maxRetries,
        RetryDelay: retryDelay,
    }
    
    // Add custom headers
    userAgent := fmt.Sprintf("terraform-provider-mycloud/%s", p.version)
    transport = &headerTransport{
        Transport: transport,
        UserAgent: userAgent,
    }
    
    // Create HTTP client
    httpClient := &http.Client{
        Timeout:   timeout,
        Transport: transport,
    }
    
    // Create API client
    endpoint := config.Endpoint.ValueString()
    if endpoint == "" {
        endpoint = "https://api.mycloud.com"
    }
    
    client := api.NewClient(api.Config{
        HTTPClient: httpClient,
        BaseURL:    endpoint,
    })
    
    // Test connection
    if err := client.TestConnection(ctx); err != nil {
        resp.Diagnostics.AddError(
            "Connection Test Failed",
            fmt.Sprintf("Could not connect to API: %s", err.Error()),
        )
        return
    }
    
    // Share with resources
    resp.ResourceData = client
    resp.DataSourceData = client
}

func parseTimeout(attr types.Number, defaultSeconds int) time.Duration {
    if attr.IsNull() {
        return time.Duration(defaultSeconds) * time.Second
    }
    seconds, _ := attr.ValueBigFloat().Int64()
    return time.Duration(seconds) * time.Second
}

func parseInt(attr types.Number, defaultValue int) int {
    if attr.IsNull() {
        return defaultValue
    }
    value, _ := attr.ValueBigFloat().Int64()
    return int(value)
}

Best Practices

1. Provide Sensible Defaults

// ✅ Good: Defaults for all timeouts
timeout := 30 * time.Second
if !config.Timeout.IsNull() {
    timeout = parseTimeout(config.Timeout)
}

2. Validate Configuration

// ✅ Validate timeout ranges
"timeout": schema.NumberAttribute{
    Optional: true,
    Validators: []validator.Number{
        numbervalidator.Between(1, 300),
    },
}

3. Test Connection

// ✅ Test connection during Configure
if err := client.TestConnection(ctx); err != nil {
    resp.Diagnostics.AddError("Connection Failed", err.Error())
}

4. Use Connection Pooling

// ✅ Configure connection pooling
transport := &http.Transport{
    MaxIdleConns:        100,
    MaxIdleConnsPerHost: 10,
    IdleConnTimeout:     90 * time.Second,
}

5. Implement Retry Logic

// ✅ Retry on transient errors
func shouldRetry(statusCode int) bool {
    return statusCode >= 500 || statusCode == 429
}

6. Log Configuration

// ✅ Log configuration (without secrets)
tflog.Debug(ctx, "Configuring HTTP client", map[string]any{
    "timeout":      timeout.Seconds(),
    "max_retries":  maxRetries,
    "endpoint":     endpoint,
})

Key Takeaways

  • ✅ Configure appropriate timeouts for all operations
  • ✅ Implement retry logic for transient failures
  • ✅ Use connection pooling for performance
  • ✅ Support custom headers for flexibility
  • ✅ Configure TLS properly for security
  • ✅ Test connections during provider configuration
  • ✅ Provide sensible defaults
  • ✅ Document all configuration options
  • ✅ Use layered transports for separation of concerns

What’s Next?

Now let’s learn about handling configuration from multiple sources.

Next: Configuration Sources →


Previous: ← Authentication Methods

Last updated: June 30, 2026

Updated: