Skip to content

feat: Add WithHttpClient option to allow custom http.Client #2785

@rv-ekroener

Description

@rv-ekroener

Component(s)

router

Is your feature request related to a problem? Please describe.

The router-plugin/httpclient package does not expose a way to supply a custom *http.Client, despite the internal Client struct already having a client *http.Client field.

Currently, the New() function hardcodes http.DefaultClient:

c := &Client{
    client: http.DefaultClient,
    // ...
}

This forces developers to modify http.DefaultClient directly for use cases like disabling redirects:

// Current workaround: modifying global default client
http.DefaultClient = &http.Client{
    CheckRedirect: func(req *http.Request, via []*http.Request) error {
        return http.ErrUseLastResponse
    },
}

client := httpclient.New(
    httpclient.WithBaseURL(baseURL),
    // ...
)

This is problematic because:

  1. It affects all HTTP clients in the process globally
  2. It's not thread-safe
  3. WithTimeout() also mutates http.DefaultClient.Timeout, compounding the issue

Describe the solution you'd like

Add a WithHttpClient option:

func WithHttpClient(client *http.Client) ClientOption {
    return func(c *Client) {
        c.client = client
    }
}

Usage:

customClient := &http.Client{
    CheckRedirect: func(req *http.Request, via []*http.Request) error {
        return http.ErrUseLastResponse
    },
    Transport: myCustomTransport,
    Timeout:   10 * time.Second,
}

client := httpclient.New(
    httpclient.WithBaseURL("https://api.example.com"),
    httpclient.WithHttpClient(customClient),
)

Describe alternatives you've considered

  1. Modifying http.DefaultClient globally — Current workaround. Affects all HTTP clients, not thread-safe.

  2. Adding individual options (e.g., WithDisableRedirects, WithTransport) — Less flexible than a single BYO client option.

Additional context

Use cases this would enable:

  • Disabling redirects — Required when APIs return data via redirect headers (e.g., Location on 302)
  • HTTP request/response logging — Inject a logging RoundTripper
  • Custom TLS configuration — Client certificates, custom CA pools
  • Custom connection pooling — Tune MaxIdleConns, IdleConnTimeout
  • Proxy configuration — Per-client proxy settings
  • Testing — Inject mock transports for unit tests

Implementation notes:

  • The Client struct already has the client *http.Client field — this is just exposing it
  • Non-breaking change — existing behavior unchanged if option is not used
  • Consider: if WithHttpClient is used alongside WithTimeout, should timeout be applied to the custom client or ignored? (Suggest: ignore, let the user control their own client fully)

Prior art:

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions