diff --git a/CHANGELOG.md b/CHANGELOG.md
index ce2b2fa5..09786290 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,8 @@
### Change
- `dpr` processing option doesn't enlarge image unless `enlarge` is true.
- `304 Not Modified` responses includes `Cache-Control`, `Expires`, and `Vary` headers.
+- imgproxy responds with `500` HTTP code when the source image downloading error seems temporary (timeout, server error, etc).
+- When `IMGPROXY_FALLBACK_IMAGE_HTTP_CODE` is zero, imgproxy responds with the usual HTTP code.
### Fix
- Fix Client Hints behavior. `Width` is physical size, so we should divide it by `DPR` value.
diff --git a/docs/configuration.md b/docs/configuration.md
index 10d98f97..93096dce 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -221,7 +221,7 @@ You can set up a fallback image that will be used in case imgproxy can't fetch t
* `IMGPROXY_FALLBACK_IMAGE_DATA`: Base64-encoded image data. You can easily calculate it with `base64 tmp/fallback.png | tr -d '\n'`;
* `IMGPROXY_FALLBACK_IMAGE_PATH`: path to the locally stored image;
* `IMGPROXY_FALLBACK_IMAGE_URL`: fallback image URL.
-* `IMGPROXY_FALLBACK_IMAGE_HTTP_CODE`: HTTP code for the fallback image response. Default: `200`.
+* `IMGPROXY_FALLBACK_IMAGE_HTTP_CODE`: HTTP code for the fallback image response. When set to zero, imgproxy will respond with the usual HTTP code. Default: `200`.
* `IMGPROXY_FALLBACK_IMAGES_CACHE_SIZE`: size of custom fallback images cache. When set to `0`, fallback images cache is disabled. By default 256 fallback images are cached.
## Skip processing
diff --git a/imagedata/download.go b/imagedata/download.go
index 1d923d29..69e6a871 100644
--- a/imagedata/download.go
+++ b/imagedata/download.go
@@ -21,6 +21,11 @@ import (
var (
downloadClient *http.Client
+ enabledSchemes = map[string]struct{}{
+ "http": {},
+ "https": {},
+ }
+
imageHeadersToStore = []string{
"Cache-Control",
"Expires",
@@ -55,15 +60,20 @@ func initDownloading() error {
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
}
+ registerProtocol := func(scheme string, rt http.RoundTripper) {
+ transport.RegisterProtocol(scheme, rt)
+ enabledSchemes[scheme] = struct{}{}
+ }
+
if config.LocalFileSystemRoot != "" {
- transport.RegisterProtocol("local", fsTransport.New())
+ registerProtocol("local", fsTransport.New())
}
if config.S3Enabled {
if t, err := s3Transport.New(); err != nil {
return err
} else {
- transport.RegisterProtocol("s3", t)
+ registerProtocol("s3", t)
}
}
@@ -71,7 +81,7 @@ func initDownloading() error {
if t, err := gcsTransport.New(); err != nil {
return err
} else {
- transport.RegisterProtocol("gs", t)
+ registerProtocol("gs", t)
}
}
@@ -79,7 +89,7 @@ func initDownloading() error {
if t, err := azureTransport.New(); err != nil {
return err
} else {
- transport.RegisterProtocol("abs", t)
+ registerProtocol("abs", t)
}
}
@@ -109,6 +119,14 @@ func requestImage(imageURL string, header http.Header) (*http.Response, error) {
return nil, ierrors.New(404, err.Error(), msgSourceImageIsUnreachable).SetUnexpected(config.ReportDownloadingErrors)
}
+ if _, ok := enabledSchemes[req.URL.Scheme]; !ok {
+ return nil, ierrors.New(
+ 404,
+ fmt.Sprintf("Unknown sheme: %s", req.URL.Scheme),
+ msgSourceImageIsUnreachable,
+ ).SetUnexpected(config.ReportDownloadingErrors)
+ }
+
req.Header.Set("User-Agent", config.UserAgent)
for k, v := range header {
@@ -119,7 +137,7 @@ func requestImage(imageURL string, header http.Header) (*http.Response, error) {
res, err := downloadClient.Do(req)
if err != nil {
- return res, ierrors.New(404, checkTimeoutErr(err).Error(), msgSourceImageIsUnreachable).SetUnexpected(config.ReportDownloadingErrors)
+ return nil, ierrors.New(500, checkTimeoutErr(err).Error(), msgSourceImageIsUnreachable).SetUnexpected(config.ReportDownloadingErrors)
}
if res.StatusCode == http.StatusNotModified {
@@ -130,8 +148,13 @@ func requestImage(imageURL string, header http.Header) (*http.Response, error) {
body, _ := ioutil.ReadAll(res.Body)
res.Body.Close()
+ status := 404
+ if res.StatusCode >= 500 {
+ status = 500
+ }
+
msg := fmt.Sprintf("Status: %d; %s", res.StatusCode, string(body))
- return res, ierrors.New(404, msg, msgSourceImageIsUnreachable).SetUnexpected(config.ReportDownloadingErrors)
+ return nil, ierrors.New(status, msg, msgSourceImageIsUnreachable).SetUnexpected(config.ReportDownloadingErrors)
}
return res, nil
@@ -168,7 +191,7 @@ func download(imageURL string, header http.Header) (*ImageData, error) {
imgdata, err := readAndCheckImage(body, contentLength)
if err != nil {
- return nil, err
+ return nil, ierrors.Wrap(err, 0).SetUnexpected(config.ReportDownloadingErrors)
}
imgdata.Headers = headersToStore(res)
diff --git a/imagedata/read.go b/imagedata/read.go
index 04813644..818a0bdd 100644
--- a/imagedata/read.go
+++ b/imagedata/read.go
@@ -58,7 +58,7 @@ func readAndCheckImage(r io.Reader, contentLength int) (*ImageData, error) {
return nil, ErrSourceImageTypeNotSupported
}
if err != nil {
- return nil, ierrors.Wrap(checkTimeoutErr(err), 0)
+ return nil, checkTimeoutErr(err)
}
if err = security.CheckDimensions(meta.Width(), meta.Height()); err != nil {
@@ -67,7 +67,7 @@ func readAndCheckImage(r io.Reader, contentLength int) (*ImageData, error) {
if err = br.Flush(); err != nil {
cancel()
- return nil, ierrors.New(404, checkTimeoutErr(err).Error(), msgSourceImageIsUnreachable).SetUnexpected(config.ReportDownloadingErrors)
+ return nil, checkTimeoutErr(err)
}
return &ImageData{
diff --git a/processing_handler.go b/processing_handler.go
index a74028fc..f0a1bba8 100644
--- a/processing_handler.go
+++ b/processing_handler.go
@@ -30,8 +30,6 @@ var (
headerVaryValue string
)
-type fallbackImageUsedCtxKey struct{}
-
func initProcessingHandler() {
processingSem = make(chan struct{}, config.Concurrency)
@@ -79,7 +77,7 @@ func setVary(rw http.ResponseWriter) {
}
}
-func respondWithImage(reqID string, r *http.Request, rw http.ResponseWriter, resultData *imagedata.ImageData, po *options.ProcessingOptions, originURL string, originData *imagedata.ImageData) {
+func respondWithImage(reqID string, r *http.Request, rw http.ResponseWriter, statusCode int, resultData *imagedata.ImageData, po *options.ProcessingOptions, originURL string, originData *imagedata.ImageData) {
var contentDisposition string
if len(po.Filename) > 0 {
contentDisposition = resultData.Type.ContentDisposition(po.Filename)
@@ -111,10 +109,6 @@ func respondWithImage(reqID string, r *http.Request, rw http.ResponseWriter, res
}
rw.Header().Set("Content-Length", strconv.Itoa(len(resultData.Data)))
- statusCode := 200
- if getFallbackImageUsed(r.Context()) {
- statusCode = config.FallbackImageHTTPCode
- }
rw.WriteHeader(statusCode)
rw.Write(resultData.Data)
@@ -215,6 +209,8 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
}
defer func() { <-processingSem }()
+ statusCode := http.StatusOK
+
originData, err := func() (*imagedata.ImageData, error) {
defer metrics.StartDownloadingSegment(ctx)()
return imagedata.Download(imageURL, "source image", imgRequestHeader)
@@ -227,7 +223,11 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
respondWithNotModified(reqID, r, rw, po, imageURL, nmErr.Headers)
return
} else {
- if ierr, ok := err.(*ierrors.Error); !ok || ierr.Unexpected {
+ ierr, ierrok := err.(*ierrors.Error)
+ if ierrok {
+ statusCode = ierr.StatusCode
+ }
+ if !ierrok || ierr.Unexpected {
errorreport.Report(err, r)
}
@@ -238,13 +238,15 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
}
log.Warningf("Could not load image %s. Using fallback image. %s", imageURL, err.Error())
- r = r.WithContext(setFallbackImageUsedCtx(r.Context()))
+ if config.FallbackImageHTTPCode > 0 {
+ statusCode = config.FallbackImageHTTPCode
+ }
originData = imagedata.FallbackImage
}
router.CheckTimeout(ctx)
- if config.ETagEnabled && !getFallbackImageUsed(ctx) {
+ if config.ETagEnabled && statusCode == http.StatusOK {
imgDataMatch := etagHandler.SetActualImageData(originData)
rw.Header().Set("ETag", etagHandler.GenerateActualETag())
@@ -260,14 +262,14 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
if originData.Type == po.Format || po.Format == imagetype.Unknown {
// Don't process SVG
if originData.Type == imagetype.SVG {
- respondWithImage(reqID, r, rw, originData, po, imageURL, originData)
+ respondWithImage(reqID, r, rw, statusCode, originData, po, imageURL, originData)
return
}
if len(po.SkipProcessingFormats) > 0 {
for _, f := range po.SkipProcessingFormats {
if f == originData.Type {
- respondWithImage(reqID, r, rw, originData, po, imageURL, originData)
+ respondWithImage(reqID, r, rw, statusCode, originData, po, imageURL, originData)
return
}
}
@@ -299,14 +301,5 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
router.CheckTimeout(ctx)
- respondWithImage(reqID, r, rw, resultData, po, imageURL, originData)
-}
-
-func setFallbackImageUsedCtx(ctx context.Context) context.Context {
- return context.WithValue(ctx, fallbackImageUsedCtxKey{}, true)
-}
-
-func getFallbackImageUsed(ctx context.Context) bool {
- result, _ := ctx.Value(fallbackImageUsedCtxKey{}).(bool)
- return result
+ respondWithImage(reqID, r, rw, statusCode, resultData, po, imageURL, originData)
}