request.go 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. package imagefetcher
  2. import (
  3. "compress/gzip"
  4. "context"
  5. "io"
  6. "net/http"
  7. "net/http/cookiejar"
  8. "net/url"
  9. "regexp"
  10. "strconv"
  11. "strings"
  12. "time"
  13. "go.withmatt.com/httpheaders"
  14. )
  15. var (
  16. // contentRangeRe Content-Range header regex to check if the response is a partial content response
  17. contentRangeRe = regexp.MustCompile(`^bytes ((\d+)-(\d+)|\*)/(\d+|\*)$`)
  18. )
  19. // Request is a struct that holds the request and cancel function for an image fetcher request
  20. type Request struct {
  21. fetcher *Fetcher // Parent ImageFetcher instance
  22. request *http.Request // HTTP request to fetch the image
  23. cancel context.CancelFunc // Request context cancel function
  24. }
  25. // Send sends the generic request and returns the http.Response or an error
  26. func (r *Request) Send() (*http.Response, error) {
  27. client := r.fetcher.newHttpClient()
  28. // Let's add a cookie jar to the client if the request URL is HTTP or HTTPS
  29. // This is necessary to pass cookie challenge for some servers.
  30. if r.request.URL.Scheme == "http" || r.request.URL.Scheme == "https" {
  31. jar, err := cookiejar.New(nil)
  32. if err != nil {
  33. return nil, err
  34. }
  35. client.Jar = jar
  36. }
  37. for {
  38. // Try request
  39. res, err := client.Do(r.request)
  40. if err == nil {
  41. return res, nil // Return successful response
  42. }
  43. // Close the response body if request was unsuccessful
  44. if res != nil && res.Body != nil {
  45. res.Body.Close()
  46. }
  47. // Retry if the error is due to a lost connection
  48. if strings.Contains(err.Error(), connectionLostError) {
  49. select {
  50. case <-r.request.Context().Done():
  51. return nil, err
  52. case <-time.After(bounceDelay):
  53. continue
  54. }
  55. }
  56. return nil, WrapError(err)
  57. }
  58. }
  59. // FetchImage fetches the image using the request and returns the response or an error.
  60. // It checks for the NotModified status and handles partial content responses.
  61. func (r *Request) FetchImage() (*http.Response, error) {
  62. res, err := r.Send()
  63. if err != nil {
  64. r.cancel()
  65. return nil, err
  66. }
  67. // Closes the response body and cancels request context
  68. cancel := func() {
  69. res.Body.Close()
  70. r.cancel()
  71. }
  72. // If the source image was not modified, close the body and NotModifiedError
  73. if res.StatusCode == http.StatusNotModified {
  74. cancel()
  75. return nil, newNotModifiedError(res.Header)
  76. }
  77. // If the source responds with 206, check if the response contains an entire image.
  78. // If not, return an error.
  79. if res.StatusCode == http.StatusPartialContent {
  80. err = checkPartialContentResponse(res)
  81. if err != nil {
  82. cancel()
  83. return nil, err
  84. }
  85. } else if res.StatusCode != http.StatusOK {
  86. body := extractErraticBody(res)
  87. cancel()
  88. return nil, newImageResponseStatusError(res.StatusCode, body)
  89. }
  90. // If the response is gzip encoded, wrap it in a gzip reader
  91. err = wrapGzipBody(res)
  92. if err != nil {
  93. cancel()
  94. return nil, err
  95. }
  96. // Wrap the response body in a bodyReader to ensure the request context
  97. // is cancelled when the body is closed
  98. res.Body = &bodyReader{
  99. body: res.Body,
  100. request: r,
  101. }
  102. return res, nil
  103. }
  104. // Cancel cancels the request context
  105. func (r *Request) Cancel() {
  106. r.cancel()
  107. }
  108. // URL returns the actual URL of the request
  109. func (r *Request) URL() *url.URL {
  110. return r.request.URL
  111. }
  112. // checkPartialContentResponse if the response is a partial content response,
  113. // we check if it contains the entire image.
  114. func checkPartialContentResponse(res *http.Response) error {
  115. contentRange := res.Header.Get(httpheaders.ContentRange)
  116. rangeParts := contentRangeRe.FindStringSubmatch(contentRange)
  117. if len(rangeParts) == 0 {
  118. return newImagePartialResponseError("Partial response with invalid Content-Range header")
  119. }
  120. if rangeParts[1] == "*" || rangeParts[2] != "0" {
  121. return newImagePartialResponseError("Partial response with incomplete content")
  122. }
  123. contentLengthStr := rangeParts[4]
  124. if contentLengthStr == "*" {
  125. contentLengthStr = res.Header.Get(httpheaders.ContentLength)
  126. }
  127. contentLength, _ := strconv.Atoi(contentLengthStr)
  128. rangeEnd, _ := strconv.Atoi(rangeParts[3])
  129. if contentLength <= 0 || rangeEnd != contentLength-1 {
  130. return newImagePartialResponseError("Partial response with incomplete content")
  131. }
  132. return nil
  133. }
  134. // extractErraticBody extracts the error body from the response if it is a text-based content type
  135. func extractErraticBody(res *http.Response) string {
  136. if strings.HasPrefix(res.Header.Get(httpheaders.ContentType), "text/") {
  137. bbody, _ := io.ReadAll(io.LimitReader(res.Body, 1024))
  138. return string(bbody)
  139. }
  140. return ""
  141. }
  142. // wrapGzipBody wraps the response body in a gzip reader if the Content-Encoding is gzip.
  143. // We set DisableCompression: true to avoid sending the Accept-Encoding: gzip header,
  144. // since we do not want to compress image data (which is usually already compressed).
  145. // However, some servers still send gzip-encoded responses regardless.
  146. func wrapGzipBody(res *http.Response) error {
  147. if res.Header.Get(httpheaders.ContentEncoding) == "gzip" {
  148. gzipBody, err := gzip.NewReader(res.Body)
  149. if err != nil {
  150. return nil
  151. }
  152. res.Body = gzipBody
  153. res.Header.Del(httpheaders.ContentEncoding)
  154. }
  155. return nil
  156. }
  157. // bodyReader is a wrapper around io.ReadCloser which closes original request context
  158. // when the body is closed.
  159. type bodyReader struct {
  160. body io.ReadCloser // The body to read from
  161. request *Request
  162. }
  163. // Read reads data from the response body into the provided byte slice
  164. func (r *bodyReader) Read(p []byte) (int, error) {
  165. return r.body.Read(p)
  166. }
  167. // Close closes the response body and cancels the request context
  168. func (r *bodyReader) Close() error {
  169. defer r.request.cancel()
  170. return r.body.Close()
  171. }