Skip to content

Commit

Permalink
feature: add default fallback image when failed to load from storage (#6
Browse files Browse the repository at this point in the history
) (#7)


---------

Co-authored-by: Agung Hariadi Tedja <[email protected]>
  • Loading branch information
2 people authored and aslam-kumparan committed Jan 20, 2025
1 parent b352562 commit dcc3dea
Show file tree
Hide file tree
Showing 7 changed files with 53 additions and 2 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion blob.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package imagor
import (
"bytes"
"encoding/json"
"errors"
"github.com/kumparan/imagor/fanoutreader"
"github.com/kumparan/imagor/seekstream"
"io"
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -122,6 +123,7 @@ func NewImagor(
imagor.WithUnsafe(*imagorUnsafe),
imagor.WithLogger(logger),
imagor.WithDebug(isDebug),
imagor.WithImageErrorFallback(*imagorImageErrorFallback),
)...)
}

Expand Down
4 changes: 4 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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)
Expand Down
17 changes: 16 additions & 1 deletion imagor.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ type Imagor struct {
BaseParams string
Logger *zap.Logger
Debug bool
ImageErrorFallback string

g singleflight.Group
sema *semaphore.Weighted
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
Expand Down
20 changes: 20 additions & 0 deletions imagor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
7 changes: 7 additions & 0 deletions option.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

0 comments on commit dcc3dea

Please sign in to comment.