request.go 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. package fetcher
  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. "github.com/imgproxy/imgproxy/v3/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. // Fetch fetches the image using the request and returns the response or an error.
  60. // Unlike Send, it checks the request status and converts it into typed errors.
  61. // Specifically, it checks for Not Modified, ensures that Partial Content response
  62. // contains the entire image, and wraps gzip-encoded responses.
  63. func (r *Request) Fetch() (*http.Response, error) {
  64. res, err := r.Send()
  65. if err != nil {
  66. return nil, err
  67. }
  68. // If the source image was not modified, close the body and NotModifiedError
  69. if res.StatusCode == http.StatusNotModified {
  70. res.Body.Close()
  71. return nil, newNotModifiedError(res.Header)
  72. }
  73. // If the source responds with 206, check if the response contains an entire image.
  74. // If not, return an error.
  75. if res.StatusCode == http.StatusPartialContent {
  76. err = checkPartialContentResponse(res)
  77. if err != nil {
  78. res.Body.Close()
  79. return nil, err
  80. }
  81. } else if res.StatusCode != http.StatusOK {
  82. body := extractErraticBody(res)
  83. res.Body.Close()
  84. return nil, newImageResponseStatusError(res.StatusCode, body)
  85. }
  86. // If the response is gzip encoded, wrap it in a gzip reader
  87. err = wrapGzipBody(res)
  88. if err != nil {
  89. res.Body.Close()
  90. return nil, err
  91. }
  92. return res, nil
  93. }
  94. // Cancel cancels the request context
  95. func (r *Request) Cancel() {
  96. r.cancel()
  97. }
  98. // URL returns the actual URL of the request
  99. func (r *Request) URL() *url.URL {
  100. return r.request.URL
  101. }
  102. // checkPartialContentResponse if the response is a partial content response,
  103. // we check if it contains the entire image.
  104. func checkPartialContentResponse(res *http.Response) error {
  105. contentRange := res.Header.Get(httpheaders.ContentRange)
  106. rangeParts := contentRangeRe.FindStringSubmatch(contentRange)
  107. if len(rangeParts) == 0 {
  108. return newImagePartialResponseError("Partial response with invalid Content-Range header")
  109. }
  110. if rangeParts[1] == "*" || rangeParts[2] != "0" {
  111. return newImagePartialResponseError("Partial response with incomplete content")
  112. }
  113. contentLengthStr := rangeParts[4]
  114. if contentLengthStr == "*" {
  115. contentLengthStr = res.Header.Get(httpheaders.ContentLength)
  116. }
  117. contentLength, _ := strconv.Atoi(contentLengthStr)
  118. rangeEnd, _ := strconv.Atoi(rangeParts[3])
  119. if contentLength <= 0 || rangeEnd != contentLength-1 {
  120. return newImagePartialResponseError("Partial response with incomplete content")
  121. }
  122. return nil
  123. }
  124. // extractErraticBody extracts the error body from the response if it is a text-based content type
  125. func extractErraticBody(res *http.Response) string {
  126. if strings.HasPrefix(res.Header.Get(httpheaders.ContentType), "text/") {
  127. bbody, _ := io.ReadAll(io.LimitReader(res.Body, 1024))
  128. return string(bbody)
  129. }
  130. return ""
  131. }
  132. // wrapGzipBody wraps the response body in a gzip reader if the Content-Encoding is gzip.
  133. // We set DisableCompression: true to avoid sending the Accept-Encoding: gzip header,
  134. // since we do not want to compress image data (which is usually already compressed).
  135. // However, some servers still send gzip-encoded responses regardless.
  136. func wrapGzipBody(res *http.Response) error {
  137. if res.Header.Get(httpheaders.ContentEncoding) == "gzip" {
  138. gzipBody, err := gzip.NewReader(res.Body)
  139. if err != nil {
  140. return nil
  141. }
  142. res.Body = &gzipReadCloser{
  143. Reader: gzipBody,
  144. r: res.Body,
  145. }
  146. res.Header.Del(httpheaders.ContentEncoding)
  147. }
  148. return nil
  149. }
  150. // gzipReadCloser is a wrapper around gzip.Reader which also closes the original body
  151. type gzipReadCloser struct {
  152. *gzip.Reader
  153. r io.ReadCloser
  154. }
  155. // Close closes the gzip reader and the original body
  156. func (gr *gzipReadCloser) Close() error {
  157. gr.Reader.Close()
  158. return gr.r.Close()
  159. }