http_client.go 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. // Copyright 2015 CoreOS, Inc.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package pkg
  15. import (
  16. "errors"
  17. "fmt"
  18. "io/ioutil"
  19. "log"
  20. "net/http"
  21. neturl "net/url"
  22. "strings"
  23. "time"
  24. )
  25. const (
  26. HTTP2xx = 2
  27. HTTP4xx = 4
  28. )
  29. type Err error
  30. type ErrTimeout struct {
  31. Err
  32. }
  33. type ErrNotFound struct {
  34. Err
  35. }
  36. type ErrInvalid struct {
  37. Err
  38. }
  39. type ErrServer struct {
  40. Err
  41. }
  42. type ErrNetwork struct {
  43. Err
  44. }
  45. type HTTPClient struct {
  46. // Initial backoff duration. Defaults to 50 milliseconds
  47. InitialBackoff time.Duration
  48. // Maximum exp backoff duration. Defaults to 5 seconds
  49. MaxBackoff time.Duration
  50. // Maximum number of connection retries. Defaults to 15
  51. MaxRetries int
  52. // Headers to add to the request.
  53. Header http.Header
  54. client *http.Client
  55. }
  56. type Getter interface {
  57. Get(string) ([]byte, error)
  58. GetRetry(string) ([]byte, error)
  59. }
  60. func NewHTTPClient() *HTTPClient {
  61. return NewHTTPClientHeader(nil)
  62. }
  63. func NewHTTPClientHeader(header http.Header) *HTTPClient {
  64. hc := &HTTPClient{
  65. InitialBackoff: 50 * time.Millisecond,
  66. MaxBackoff: time.Second * 5,
  67. MaxRetries: 15,
  68. Header: header,
  69. client: &http.Client{
  70. Timeout: 10 * time.Second,
  71. },
  72. }
  73. return hc
  74. }
  75. func ExpBackoff(interval, max time.Duration) time.Duration {
  76. interval = interval * 2
  77. if interval > max {
  78. interval = max
  79. }
  80. return interval
  81. }
  82. // GetRetry fetches a given URL with support for exponential backoff and maximum retries
  83. func (h *HTTPClient) GetRetry(rawurl string) ([]byte, error) {
  84. if rawurl == "" {
  85. return nil, ErrInvalid{errors.New("URL is empty. Skipping")}
  86. }
  87. url, err := neturl.Parse(rawurl)
  88. if err != nil {
  89. return nil, ErrInvalid{err}
  90. }
  91. // Unfortunately, url.Parse is too generic to throw errors if a URL does not
  92. // have a valid HTTP scheme. So, we have to do this extra validation
  93. if !strings.HasPrefix(url.Scheme, "http") {
  94. return nil, ErrInvalid{fmt.Errorf("URL %s does not have a valid HTTP scheme. Skipping", rawurl)}
  95. }
  96. dataURL := url.String()
  97. duration := h.InitialBackoff
  98. for retry := 1; retry <= h.MaxRetries; retry++ {
  99. log.Printf("Fetching data from %s. Attempt #%d", dataURL, retry)
  100. data, err := h.Get(dataURL)
  101. switch err.(type) {
  102. case ErrNetwork:
  103. log.Printf(err.Error())
  104. case ErrServer:
  105. log.Printf(err.Error())
  106. case ErrNotFound:
  107. return data, err
  108. default:
  109. return data, err
  110. }
  111. duration = ExpBackoff(duration, h.MaxBackoff)
  112. log.Printf("Sleeping for %v...", duration)
  113. time.Sleep(duration)
  114. }
  115. return nil, ErrTimeout{fmt.Errorf("Unable to fetch data. Maximum retries reached: %d", h.MaxRetries)}
  116. }
  117. func (h *HTTPClient) Get(dataURL string) ([]byte, error) {
  118. request, err := http.NewRequest("GET", dataURL, nil)
  119. if err != nil {
  120. return nil, err
  121. }
  122. request.Header = h.Header
  123. if resp, err := h.client.Do(request); err == nil {
  124. defer resp.Body.Close()
  125. switch resp.StatusCode / 100 {
  126. case HTTP2xx:
  127. return ioutil.ReadAll(resp.Body)
  128. case HTTP4xx:
  129. return nil, ErrNotFound{fmt.Errorf("Not found. HTTP status code: %d", resp.StatusCode)}
  130. default:
  131. return nil, ErrServer{fmt.Errorf("Server error. HTTP status code: %d", resp.StatusCode)}
  132. }
  133. } else {
  134. return nil, ErrNetwork{fmt.Errorf("Unable to fetch data: %s", err.Error())}
  135. }
  136. }