| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778 |
- // Package httputil provides shared HTTP clients for cloud pricing ingestion.
- //
- // The default net/http client has no timeout, so a hung or unreachable provider
- // pricing endpoint can block pricing refresh indefinitely. These helpers return
- // clients with sensible timeouts. They are shared and reused so a new connection
- // pool is not created on every pricing fetch.
- package httputil
- import (
- "context"
- "net"
- "net/http"
- "time"
- )
- // PricingTimeout is applied to pricing HTTP requests. For the bounded client it
- // caps the whole request; for the streaming client it caps the wait for
- // response headers only (not the body read).
- const PricingTimeout = 30 * time.Second
- var (
- boundedClient = &http.Client{Timeout: PricingTimeout}
- streamingClient = newStreamingClient(http.DefaultTransport)
- )
- // BoundedClient returns a shared http.Client with a total request timeout,
- // suitable for small or bounded pricing API responses.
- func BoundedClient() *http.Client {
- return boundedClient
- }
- // StreamingClient returns a shared http.Client for large pricing downloads (for
- // example the AWS pricing file or the Azure price sheet). It bounds the connect,
- // TLS handshake, and response-header wait, but not the total body read, so a
- // legitimately large or slow download is not truncated while a hung endpoint is
- // still abandoned.
- func StreamingClient() *http.Client {
- return streamingClient
- }
- // StreamingGet issues a GET for a large download using the streaming client and
- // the caller's context, so the request is cancelable. It centralizes the
- // context-aware request construction shared by the large-download paths (the AWS
- // pricing file and the Azure price sheet). Callers without a context of their
- // own can pass context.Background().
- func StreamingGet(ctx context.Context, url string) (*http.Response, error) {
- req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
- if err != nil {
- return nil, err
- }
- return StreamingClient().Do(req)
- }
- // newStreamingClient builds the streaming client from a base RoundTripper. It
- // takes the base transport as a parameter so the fallback path can be exercised
- // by tests; production passes http.DefaultTransport.
- func newStreamingClient(base http.RoundTripper) *http.Client {
- // Clone the base transport when possible so we keep its dial and TLS
- // handshake timeouts. Guard the type assertion: http.DefaultTransport is
- // declared as a RoundTripper and can be replaced (e.g. in tests), in which
- // case we fall back to a fresh transport rather than panicking.
- transport, ok := base.(*http.Transport)
- if ok {
- transport = transport.Clone()
- } else {
- // base is not a *http.Transport, so we can't clone its timeouts. Build a
- // fresh transport that still bounds dial and TLS handshake time, matching
- // the guarantee in StreamingClient's doc.
- transport = &http.Transport{
- Proxy: http.ProxyFromEnvironment,
- DialContext: (&net.Dialer{Timeout: 30 * time.Second, KeepAlive: 30 * time.Second}).DialContext,
- TLSHandshakeTimeout: 10 * time.Second,
- ExpectContinueTimeout: 1 * time.Second,
- }
- }
- transport.ResponseHeaderTimeout = PricingTimeout
- return &http.Client{Transport: transport}
- }
|