From dcc3deac7840e69f559c90f565936de861f6fae9 Mon Sep 17 00:00:00 2001 From: Tedja Date: Thu, 16 Jan 2025 09:43:20 +0700 Subject: [PATCH] feature: add default fallback image when failed to load from storage (#6) (#7) --------- Co-authored-by: Agung Hariadi Tedja --- README.md | 2 ++ blob.go | 3 ++- config/config.go | 2 ++ config/config_test.go | 4 ++++ imagor.go | 17 ++++++++++++++++- imagor_test.go | 20 ++++++++++++++++++++ option.go | 7 +++++++ 7 files changed, 53 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6578cdb21..97954c107 100644 --- a/README.md +++ b/README.md @@ -614,6 +614,8 @@ Usage of imagor: imagor disable /params endpoint -imagor-disable-error-body imagor disable response body on error + -imagor-image-error-fallback + imagor image fallback in base64 when error loading image from storage -server-address string Server address diff --git a/blob.go b/blob.go index 0241dfc73..b97afa9d6 100644 --- a/blob.go +++ b/blob.go @@ -3,6 +3,7 @@ package imagor import ( "bytes" "encoding/json" + "errors" "github.com/kumparan/imagor/fanoutreader" "github.com/kumparan/imagor/seekstream" "io" @@ -293,7 +294,7 @@ func (b *Blob) doInit() { b.blobType = BlobTypeEmpty } if err != nil && - err != io.ErrUnexpectedEOF && + !errors.Is(err, io.ErrUnexpectedEOF) && err != io.EOF { if b.err == nil { b.err = err diff --git a/config/config.go b/config/config.go index 7f00410bb..70e3a32cf 100644 --- a/config/config.go +++ b/config/config.go @@ -66,6 +66,7 @@ func NewImagor( "Check modified time of result image against the source image. This eliminates stale result but require more lookups") imagorDisableErrorBody = fs.Bool("imagor-disable-error-body", false, "imagor disable response body on error") imagorDisableParamsEndpoint = fs.Bool("imagor-disable-params-endpoint", false, "imagor disable /params endpoint") + imagorImageErrorFallback = fs.String("imagor-image-error-fallback", "", "imagor image error fallback when failed to load from storage, in base64") imagorSignerType = fs.String("imagor-signer-type", "sha1", "imagor URL signature hasher type: sha1, sha256, sha512") imagorSignerTruncate = fs.Int("imagor-signer-truncate", 0, "imagor URL signature truncate at length") imagorStoragePathStyle = fs.String("imagor-storage-path-style", "original", "imagor storage path style: original, digest") @@ -122,6 +123,7 @@ func NewImagor( imagor.WithUnsafe(*imagorUnsafe), imagor.WithLogger(logger), imagor.WithDebug(isDebug), + imagor.WithImageErrorFallback(*imagorImageErrorFallback), )...) } diff --git a/config/config_test.go b/config/config_test.go index 8fcdbca36..2c1bc0dec 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -41,6 +41,8 @@ func TestDefault(t *testing.T) { } func TestBasic(t *testing.T) { + placeholderData := `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` + srv := CreateServer([]string{ "-debug", "-port", "2345", @@ -59,6 +61,7 @@ func TestBasic(t *testing.T) { "-imagor-base-params", "filters:watermark(example.jpg)", "-imagor-cache-header-ttl", "169h", "-imagor-cache-header-swr", "167h", + "-imagor-image-error-fallback", placeholderData, "-http-loader-insecure-skip-verify-transport", "-http-loader-override-response-headers", "cache-control,content-type", "-http-loader-base-url", "https://www.example.com/foo.org", @@ -82,6 +85,7 @@ func TestBasic(t *testing.T) { assert.Equal(t, "filters:watermark(example.jpg)/", app.BaseParams) assert.Equal(t, time.Hour*169, app.CacheHeaderTTL) assert.Equal(t, time.Hour*167, app.CacheHeaderSWR) + assert.Equal(t, placeholderData, app.ImageErrorFallback) httpLoader := app.Loaders[0].(*httploader.HTTPLoader) assert.True(t, httpLoader.Transport.(*http.Transport).TLSClientConfig.InsecureSkipVerify) diff --git a/imagor.go b/imagor.go index 3922881ce..46a12de5a 100644 --- a/imagor.go +++ b/imagor.go @@ -89,6 +89,7 @@ type Imagor struct { BaseParams string Logger *zap.Logger Debug bool + ImageErrorFallback string g singleflight.Group sema *semaphore.Weighted @@ -171,7 +172,7 @@ func (app *Imagor) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } blob, err := checkBlob(app.Do(r, p)) - if err == ErrInvalid || err == ErrSignatureMismatch { + if errors.Is(err, ErrInvalid) || errors.Is(err, ErrSignatureMismatch) { if path2, e := url.QueryUnescape(path); e == nil { path = path2 p = imagorpath.Parse(path) @@ -521,11 +522,25 @@ func fromStorages( func (app *Imagor) loadStorage(r *http.Request, key string, isBase64 bool) (blob *Blob, shouldSave bool, err error) { r = app.requestWithLoadContext(r) + var origin Storage blob, origin, err = app.fromStoragesAndLoaders(r, app.Storages, app.Loaders, key, isBase64) if !isBlobEmpty(blob) && origin == nil && key != "" && err == nil && len(app.Storages) > 0 { shouldSave = true + app.Logger.Error("fail to load from storage", zap.String("key", key), zap.Error(err)) + } + + if (err != nil || isBlobEmpty(blob)) && len(app.ImageErrorFallback) > 0 { + data, errDecode := base64.StdEncoding.DecodeString(app.ImageErrorFallback) + if errDecode != nil { + app.Logger.Error("failed to decode base64", zap.Error(errDecode)) + } + + mimeType := http.DetectContentType(data) + blob = NewBlobFromBytes(data) + blob.SetContentType(mimeType) + err = nil // reset error } return } diff --git a/imagor_test.go b/imagor_test.go index badb69e65..31bdec4ca 100644 --- a/imagor_test.go +++ b/imagor_test.go @@ -588,6 +588,26 @@ func TestParams(t *testing.T) { assert.Empty(t, w.Body.String()) } +func TestUseFallbackImageWhenLoadError(t *testing.T) { + loader := loaderFunc(func(r *http.Request, image string) (*Blob, error) { + return NewBlobFromFile("./non-exists-path"), nil + }) + placeholderData := `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` + + app := New( + WithDebug(true), + WithLogger(zap.NewExample()), + WithUnsafe(true), + WithLoaders(loader), + WithImageErrorFallback(placeholderData)) + + r := httptest.NewRequest( + http.MethodGet, "https://example.com/unsafe/foobar", nil) + w := httptest.NewRecorder() + app.ServeHTTP(w, r) + assert.Equal(t, 200, w.Code) +} + var clock time.Time type mapStore struct { diff --git a/option.go b/option.go index ecc0ca7fb..ece269b83 100644 --- a/option.go +++ b/option.go @@ -227,3 +227,10 @@ func WithSigner(signer imagorpath.Signer) Option { } } } + +// WithImageErrorFallback with image error fallback option +func WithImageErrorFallback(base64Image string) Option { + return func(app *Imagor) { + app.ImageErrorFallback = base64Image + } +}