httputil.go 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778
  1. // Package httputil provides shared HTTP clients for cloud pricing ingestion.
  2. //
  3. // The default net/http client has no timeout, so a hung or unreachable provider
  4. // pricing endpoint can block pricing refresh indefinitely. These helpers return
  5. // clients with sensible timeouts. They are shared and reused so a new connection
  6. // pool is not created on every pricing fetch.
  7. package httputil
  8. import (
  9. "context"
  10. "net"
  11. "net/http"
  12. "time"
  13. )
  14. // PricingTimeout is applied to pricing HTTP requests. For the bounded client it
  15. // caps the whole request; for the streaming client it caps the wait for
  16. // response headers only (not the body read).
  17. const PricingTimeout = 30 * time.Second
  18. var (
  19. boundedClient = &http.Client{Timeout: PricingTimeout}
  20. streamingClient = newStreamingClient(http.DefaultTransport)
  21. )
  22. // BoundedClient returns a shared http.Client with a total request timeout,
  23. // suitable for small or bounded pricing API responses.
  24. func BoundedClient() *http.Client {
  25. return boundedClient
  26. }
  27. // StreamingClient returns a shared http.Client for large pricing downloads (for
  28. // example the AWS pricing file or the Azure price sheet). It bounds the connect,
  29. // TLS handshake, and response-header wait, but not the total body read, so a
  30. // legitimately large or slow download is not truncated while a hung endpoint is
  31. // still abandoned.
  32. func StreamingClient() *http.Client {
  33. return streamingClient
  34. }
  35. // StreamingGet issues a GET for a large download using the streaming client and
  36. // the caller's context, so the request is cancelable. It centralizes the
  37. // context-aware request construction shared by the large-download paths (the AWS
  38. // pricing file and the Azure price sheet). Callers without a context of their
  39. // own can pass context.Background().
  40. func StreamingGet(ctx context.Context, url string) (*http.Response, error) {
  41. req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
  42. if err != nil {
  43. return nil, err
  44. }
  45. return StreamingClient().Do(req)
  46. }
  47. // newStreamingClient builds the streaming client from a base RoundTripper. It
  48. // takes the base transport as a parameter so the fallback path can be exercised
  49. // by tests; production passes http.DefaultTransport.
  50. func newStreamingClient(base http.RoundTripper) *http.Client {
  51. // Clone the base transport when possible so we keep its dial and TLS
  52. // handshake timeouts. Guard the type assertion: http.DefaultTransport is
  53. // declared as a RoundTripper and can be replaced (e.g. in tests), in which
  54. // case we fall back to a fresh transport rather than panicking.
  55. transport, ok := base.(*http.Transport)
  56. if ok {
  57. transport = transport.Clone()
  58. } else {
  59. // base is not a *http.Transport, so we can't clone its timeouts. Build a
  60. // fresh transport that still bounds dial and TLS handshake time, matching
  61. // the guarantee in StreamingClient's doc.
  62. transport = &http.Transport{
  63. Proxy: http.ProxyFromEnvironment,
  64. DialContext: (&net.Dialer{Timeout: 30 * time.Second, KeepAlive: 30 * time.Second}).DialContext,
  65. TLSHandshakeTimeout: 10 * time.Second,
  66. ExpectContinueTimeout: 1 * time.Second,
  67. }
  68. }
  69. transport.ResponseHeaderTimeout = PricingTimeout
  70. return &http.Client{Transport: transport}
  71. }