httputil.go 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. package httputil
  2. import (
  3. "context"
  4. "fmt"
  5. "math"
  6. "net/http"
  7. "net/url"
  8. "strconv"
  9. "strings"
  10. "time"
  11. "github.com/kubecost/cost-model/pkg/util/mapper"
  12. )
  13. //--------------------------------------------------------------------------
  14. // QueryParams
  15. //--------------------------------------------------------------------------
  16. type QueryParams = mapper.PrimitiveMap
  17. // queryParamsMap is mapper.Map adapter for url.Values
  18. type queryParamsMap struct {
  19. values url.Values
  20. }
  21. // mapper.Getter implementation
  22. func (qpm *queryParamsMap) Get(key string) string {
  23. return qpm.values.Get(key)
  24. }
  25. // mapper.Setter implementation
  26. func (qpm *queryParamsMap) Set(key, value string) error {
  27. qpm.values.Set(key, value)
  28. return nil
  29. }
  30. // NewQueryParams creates a primitive map using the request query parameters
  31. func NewQueryParams(values url.Values) QueryParams {
  32. return mapper.NewMapper(&queryParamsMap{values})
  33. }
  34. //--------------------------------------------------------------------------
  35. // HTTP Context Utilities
  36. //--------------------------------------------------------------------------
  37. const (
  38. ContextWarning string = "Warning"
  39. ContextName string = "Name"
  40. ContextQuery string = "Query"
  41. )
  42. // GetWarning Extracts a warning message from the request context if it exists
  43. func GetWarning(r *http.Request) (warning string, ok bool) {
  44. warning, ok = r.Context().Value(ContextWarning).(string)
  45. return
  46. }
  47. // SetWarning Sets the warning context on the provided request and returns a new instance of the request
  48. // with the new context.
  49. func SetWarning(r *http.Request, warning string) *http.Request {
  50. ctx := context.WithValue(r.Context(), ContextWarning, warning)
  51. return r.WithContext(ctx)
  52. }
  53. // GetName Extracts a name value from the request context if it exists
  54. func GetName(r *http.Request) (name string, ok bool) {
  55. name, ok = r.Context().Value(ContextName).(string)
  56. return
  57. }
  58. // SetName Sets the name value on the provided request and returns a new instance of the request
  59. // with the new context.
  60. func SetName(r *http.Request, name string) *http.Request {
  61. ctx := context.WithValue(r.Context(), ContextName, name)
  62. return r.WithContext(ctx)
  63. }
  64. // GetQuery Extracts a query value from the request context if it exists
  65. func GetQuery(r *http.Request) (name string, ok bool) {
  66. name, ok = r.Context().Value(ContextQuery).(string)
  67. return
  68. }
  69. // SetQuery Sets the query value on the provided request and returns a new instance of the request
  70. // with the new context.
  71. func SetQuery(r *http.Request, query string) *http.Request {
  72. ctx := context.WithValue(r.Context(), ContextQuery, query)
  73. return r.WithContext(ctx)
  74. }
  75. //--------------------------------------------------------------------------
  76. // Package Funcs
  77. //--------------------------------------------------------------------------
  78. // IsRateLimited accepts a response and body to determine if either indicate
  79. // a rate limited return
  80. func IsRateLimited(resp *http.Response, body []byte) bool {
  81. return IsRateLimitedResponse(resp) || IsRateLimitedBody(resp, body)
  82. }
  83. // RateLimitedRetryFor returns the parsed Retry-After header relative to the
  84. // current time. If the Retry-After header does not exist, the defaultWait parameter
  85. // is returned.
  86. func RateLimitedRetryFor(resp *http.Response, defaultWait time.Duration, retry int) time.Duration {
  87. if resp.Header == nil {
  88. return ExponentialBackoffWaitFor(defaultWait, retry)
  89. }
  90. // Retry-After is either the number of seconds to wait or a target datetime (RFC1123)
  91. value := resp.Header.Get("Retry-After")
  92. if value == "" {
  93. return defaultWait
  94. }
  95. seconds, err := strconv.ParseInt(value, 10, 64)
  96. if err == nil {
  97. return time.Duration(seconds) * time.Second
  98. }
  99. // failed to parse an integer, try datetime RFC1123
  100. t, err := time.Parse(time.RFC1123, value)
  101. if err == nil {
  102. // return 0 if the datetime has already elapsed
  103. result := t.Sub(time.Now())
  104. if result < 0 {
  105. return 0
  106. }
  107. return result
  108. }
  109. // failed to parse datetime, return default
  110. return defaultWait
  111. }
  112. // ExpontentialBackoffWatiFor accepts a default wait duration and the current retry count
  113. // and returns a new duration
  114. func ExponentialBackoffWaitFor(defaultWait time.Duration, retry int) time.Duration {
  115. return time.Duration(math.Pow(2, float64(retry))*float64(defaultWait.Milliseconds())) * time.Millisecond
  116. }
  117. // IsRateLimitedResponse returns true if the status code is a 429 (TooManyRequests)
  118. func IsRateLimitedResponse(resp *http.Response) bool {
  119. return resp.StatusCode == http.StatusTooManyRequests
  120. }
  121. // IsRateLimitedBody attempts to determine if a response body indicates throttling
  122. // has occurred. This function is a result of some API providers (AWS) returning
  123. // a 400 status code instead of 429 for rate limit exceptions.
  124. func IsRateLimitedBody(resp *http.Response, body []byte) bool {
  125. // ignore non-400 status
  126. if resp.StatusCode < http.StatusBadRequest || resp.StatusCode >= http.StatusInternalServerError {
  127. return false
  128. }
  129. return strings.Contains(string(body), "ThrottlingException")
  130. }
  131. // HeaderString writes the request/response http.Header to a string.
  132. func HeaderString(h http.Header) string {
  133. var sb strings.Builder
  134. var first bool = true
  135. sb.WriteString("{ ")
  136. for k, vs := range h {
  137. if first {
  138. first = false
  139. } else {
  140. sb.WriteString(", ")
  141. }
  142. fmt.Fprintf(&sb, "%s: [ ", k)
  143. for idx, v := range vs {
  144. sb.WriteString(v)
  145. if idx != len(vs)-1 {
  146. sb.WriteString(", ")
  147. }
  148. }
  149. sb.WriteString(" ]")
  150. }
  151. sb.WriteString(" }")
  152. return sb.String()
  153. }