Viktor Sokolov 6 месяцев назад
Родитель
Сommit
5db788f2b1

+ 194 - 0
errwrap/err_wrap.go

@@ -0,0 +1,194 @@
+// errwrap package provides a way to wrap errors with additional context
+package errwrap
+
+import (
+	"fmt"
+	"net/http"
+	"runtime"
+	"strings"
+)
+
+const (
+	// defaultPublicMessage is the default public message for errors
+	defaultPublicMessage = "Internal error"
+
+	// defaultStatusCode is the default HTTP status code for errors
+	// (500 internal server error)
+	defaultStatusCode = http.StatusInternalServerError
+)
+
+type ErrWrap struct {
+	// err is the underlying error which is wrapped originally
+	error
+
+	// statusCode represents the HTTP status code for the error
+	statusCode int
+
+	// publicMessage is a message that is shown in the error reporting system
+	publicMessage string
+
+	// shouldReport indicates whether the error should be reported to the error reporting system
+	shouldReport bool
+
+	// stack is the original stack trace of the error
+	stack []uintptr
+
+	// messages is a list of messages associated with the error
+	messages []string
+}
+
+// New creates a New ErrWrap instance with the provided message
+func New(message string, skipStackFrames int) *ErrWrap {
+	return From(fmt.Errorf("%s", message), skipStackFrames)
+}
+
+// Newf creates a new ErrWrap instance with a formatted message
+func Errorf(skipStackFrames int, format string, args ...any) *ErrWrap {
+	return From(fmt.Errorf(format, args...), skipStackFrames)
+}
+
+// From creates a new ErrWrap instance from an existing error.
+// In case underlying error is already an ErrWrap, it will start from scratch,
+// except that it will keep the original error.
+func From(err error, skipStackFrames int) *ErrWrap {
+	// Do not re-wrap an already wrapped error
+	e := err
+
+	o, ok := err.(*ErrWrap)
+	if ok {
+		e = o.Unwrap()
+	}
+
+	return &ErrWrap{
+		error:         e,
+		statusCode:    defaultStatusCode,
+		publicMessage: defaultPublicMessage,
+		shouldReport:  true,
+		stack:         callers(skipStackFrames),
+		messages:      make([]string, 0),
+	}
+}
+
+// Clone creates a copy of the ErrWrap instance.
+func (e *ErrWrap) Clone() *ErrWrap {
+	if e == nil {
+		return nil
+	}
+
+	clone := &ErrWrap{
+		error:         e.error,
+		statusCode:    e.statusCode,
+		publicMessage: e.publicMessage,
+		shouldReport:  e.shouldReport,
+		stack:         e.stack,
+		messages:      make([]string, len(e.messages)),
+	}
+	copy(clone.messages, e.messages)
+
+	return clone
+}
+
+// Wrap wraps an existing error into an ErrWrap instance
+func Wrap(err error) *ErrWrap {
+	if err == nil {
+		return nil
+	}
+
+	var wrapped *ErrWrap
+	if existing, ok := err.(*ErrWrap); ok {
+		wrapped = existing.Clone()
+	} else {
+		wrapped = &ErrWrap{
+			error:         err,
+			statusCode:    defaultStatusCode,
+			publicMessage: defaultPublicMessage,
+			shouldReport:  true,
+			stack:         callers(0),
+			messages:      make([]string, 0),
+		}
+	}
+
+	return wrapped
+}
+
+// Wrapf wraps an existing error into an ErrWrap instance with a formatted message
+func Wrapf(err error, msg string, args ...any) *ErrWrap {
+	if err == nil {
+		return nil
+	}
+
+	wrapped := Wrap(err)
+	formatted := fmt.Sprintf(msg, args...)
+	wrapped.messages = append(wrapped.messages, formatted)
+	return wrapped
+}
+
+// Error returns the error message
+func (e *ErrWrap) Error() string {
+	if len(e.messages) > 0 {
+		return fmt.Sprintf("%s: %s", e.error.Error(), strings.Join(e.messages, ": "))
+	}
+	return e.error.Error()
+}
+
+// Unwrap returns the underlying error
+func (e *ErrWrap) Unwrap() error {
+	return e.error
+}
+
+// StatusCode returns the HTTP status code
+func (e *ErrWrap) StatusCode() int {
+	return e.statusCode
+}
+
+// PublicMessage returns the public message
+func (e *ErrWrap) PublicMessage() string {
+	return e.publicMessage
+}
+
+// ShouldReport returns whether the error should be reported
+func (e *ErrWrap) ShouldReport() bool {
+	return e.shouldReport
+}
+
+// WithStatusCode sets the HTTP status code and returns a new instance
+func (e *ErrWrap) WithStatusCode(code int) *ErrWrap {
+	newErr := e.Clone()
+	newErr.statusCode = code
+	return newErr
+}
+
+// WithPublicMessage sets the public message and returns a new instance
+func (e *ErrWrap) WithPublicMessage(msg string) *ErrWrap {
+	newErr := e.Clone()
+	newErr.publicMessage = msg
+	return newErr
+}
+
+// WithShouldReport sets whether the error should be reported and returns a new instance
+func (e *ErrWrap) WithShouldReport(report bool) *ErrWrap {
+	// Create a copy to maintain immutability
+	newErr := e.Clone()
+	newErr.shouldReport = report
+	return newErr
+}
+
+// FormatStack formats the stack trace into a human-readable string
+func (e *ErrWrap) FormatStack() string {
+	lines := make([]string, len(e.stack))
+
+	for i, pc := range e.stack {
+		f := runtime.FuncForPC(pc)
+		file, line := f.FileLine(pc)
+		lines[i] = fmt.Sprintf("%s:%d %s", file, line, f.Name())
+	}
+
+	return strings.Join(lines, "\n")
+}
+
+// callers captures the stack trace
+func callers(skip int) []uintptr {
+	stack := make([]uintptr, 10)
+	n := runtime.Callers(skip+2, stack)
+	return stack[:n]
+}

+ 252 - 0
errwrap/err_wrap_test.go

@@ -0,0 +1,252 @@
+package errwrap
+
+import (
+	"context"
+	"errors"
+	"net/http"
+	"reflect"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+type (
+	fooError struct{ error }
+	barError struct{ error }
+)
+
+// notModifiedError is a custom error type that contains HTTP headers
+type notModifiedError struct {
+	headers http.Header
+}
+
+func (e notModifiedError) Error() string {
+	return "not modified"
+}
+
+func (e notModifiedError) Headers() http.Header {
+	return e.headers
+}
+
+// Constructor for notModifiedError
+func newNotModifiedError(headers http.Header) notModifiedError {
+	return notModifiedError{headers: headers}
+}
+
+// Is performs comparison of two notModifiedError instances.
+// Any error should be Comparable, http.Header is not comparable,
+// hence, we need to compare headers manually.
+func (nm notModifiedError) Is(target error) bool {
+	m, ok := target.(notModifiedError)
+	return ok && reflect.DeepEqual(nm.headers, m.headers)
+}
+
+func TestInnerErrorWrapperIs(t *testing.T) {
+	fooInnerErr := &fooError{errors.New("inner error")}
+	barInnerErr := &barError{errors.New("inner error")}
+
+	assert.Equal(t, "inner error", fooInnerErr.Error())
+	require.NotErrorIs(t, fooInnerErr, errors.New("inner error"))
+	require.NotErrorIs(t, fooInnerErr, barInnerErr)
+}
+
+func TestInnerErrorWrapperAs(t *testing.T) {
+	fooInnerErr := fooError{errors.New("foo error")}
+	barInnerErr := barError{errors.New("foo error")}
+
+	var ie fooError
+
+	require.ErrorAs(t, fooInnerErr, &ie)
+	require.NotErrorAs(t, barInnerErr, &ie)
+	assert.Equal(t, "foo error", ie.Error())
+}
+
+func TestNew(t *testing.T) {
+	err := Errorf(0, "test error %d", 123)
+
+	assert.Equal(t, "test error 123", err.Error())
+	assert.Equal(t, http.StatusInternalServerError, err.StatusCode())
+	assert.Equal(t, "Internal error", err.PublicMessage())
+	assert.True(t, err.ShouldReport())
+	require.Error(t, err.Unwrap())
+	assert.Empty(t, err.messages) // No additional messages
+}
+
+func TestWrap(t *testing.T) {
+	originalErr := errors.New("original error")
+
+	wrappedErr := Wrap(originalErr)
+
+	assert.Equal(t, "original error", wrappedErr.Error())
+	assert.Equal(t, originalErr, wrappedErr.Unwrap())
+	assert.Equal(t, http.StatusInternalServerError, wrappedErr.StatusCode())
+	assert.Equal(t, "Internal error", wrappedErr.PublicMessage())
+	assert.True(t, wrappedErr.ShouldReport())
+}
+
+func TestWrapNil(t *testing.T) {
+	wrappedErr := Wrap(nil)
+	assert.Nil(t, wrappedErr)
+
+	wrappedErr = Wrapf(nil, "some message")
+	assert.Nil(t, wrappedErr)
+}
+
+func TestWrapAlreadyWrapped(t *testing.T) {
+	originalErr := New("original error", 0)
+
+	wrappedErr := Wrap(originalErr)
+	assert.NotSame(t, originalErr, wrappedErr)
+}
+
+func TestWrapf(t *testing.T) {
+	originalErr := errors.New("database error")
+
+	wrappedErr := Wrapf(originalErr, "failed to save user %d", 123)
+
+	assert.Equal(t, "database error: failed to save user 123", wrappedErr.Error())
+	assert.Equal(t, originalErr, wrappedErr.Unwrap())
+	assert.Len(t, wrappedErr.messages, 1)
+}
+
+func TestWrapfExistingErrWrap(t *testing.T) {
+	originalErr := New("database error", 0)
+
+	// First wrap
+	firstWrap := Wrapf(originalErr, "failed to query")
+
+	// Second wrap - should create new instance, not modify original
+	secondWrap := Wrapf(firstWrap, "failed to get user")
+
+	// Verify that Clone() was called
+	assert.NotSame(t, firstWrap, secondWrap)
+	assert.NotSame(t, originalErr, firstWrap)
+	assert.NotSame(t, originalErr, secondWrap)
+
+	assert.Equal(t, "database error", originalErr.Error())
+	assert.Empty(t, originalErr.messages)
+
+	// Check first wrap
+	assert.Equal(t, "database error: failed to query", firstWrap.Error())
+	assert.Len(t, firstWrap.messages, 1)
+	assert.Equal(t, "failed to query", firstWrap.messages[0])
+
+	// Check second wrap
+	assert.Equal(t, "database error: failed to query: failed to get user", secondWrap.Error())
+	assert.Len(t, secondWrap.messages, 2)
+	assert.Equal(t, "failed to query", secondWrap.messages[0])
+	assert.Equal(t, "failed to get user", secondWrap.messages[1])
+}
+
+func TestWithStatusCode(t *testing.T) {
+	originalErr := New("test error", 0)
+	modifiedErr := originalErr.WithStatusCode(http.StatusNotFound)
+
+	assert.Equal(t, http.StatusInternalServerError, originalErr.StatusCode())
+	assert.Equal(t, http.StatusNotFound, modifiedErr.StatusCode())
+}
+
+func TestWithPublicMessage(t *testing.T) {
+	originalErr := New("internal database error", 0)
+	modifiedErr := originalErr.WithPublicMessage("Service temporarily unavailable")
+
+	assert.Equal(t, "Internal error", originalErr.PublicMessage())
+	assert.Equal(t, "Service temporarily unavailable", modifiedErr.PublicMessage())
+}
+
+func TestWithShouldReport(t *testing.T) {
+	originalErr := New("test error", 0)
+	modifiedErr := originalErr.WithShouldReport(false)
+
+	assert.True(t, originalErr.ShouldReport())
+	assert.False(t, modifiedErr.ShouldReport())
+	assert.NotSame(t, originalErr, modifiedErr)
+}
+
+func TestChaining(t *testing.T) {
+	baseErr := errors.New("database connection failed")
+
+	finalErr := Wrapf(baseErr, "failed to save user %d", 123).
+		WithStatusCode(http.StatusInternalServerError).
+		WithPublicMessage("Unable to save changes").
+		WithShouldReport(true)
+
+	assert.Equal(t, "database connection failed: failed to save user 123", finalErr.Error())
+	assert.Equal(t, http.StatusInternalServerError, finalErr.StatusCode())
+	assert.Equal(t, "Unable to save changes", finalErr.PublicMessage())
+	assert.True(t, finalErr.ShouldReport())
+	assert.Equal(t, baseErr, finalErr.Unwrap())
+}
+
+func TestErrorsIs(t *testing.T) {
+	baseErr := errors.New("base error")
+	otherErr := errors.New("other error")
+
+	wrappedErr := Wrapf(baseErr, "wrapped error")
+
+	require.ErrorIs(t, wrappedErr, baseErr)
+	assert.NotErrorIs(t, wrappedErr, otherErr)
+}
+
+func TestErrorsAs(t *testing.T) {
+	baseErr := New("base error", 0).WithStatusCode(http.StatusAccepted)
+	wrappedErr := Wrapf(baseErr, "wrapped error")
+
+	var extractedErr *ErrWrap
+	require.ErrorAs(t, wrappedErr, &extractedErr)
+	assert.NotNil(t, extractedErr)
+
+	assert.Equal(t, baseErr.StatusCode(), extractedErr.StatusCode())
+	assert.Equal(t, baseErr.PublicMessage(), extractedErr.PublicMessage())
+	assert.Equal(t, baseErr.ShouldReport(), extractedErr.ShouldReport())
+}
+
+func TestStackTracePreservation(t *testing.T) {
+	originalErr := New("original", 0)
+	wrappedErr := Wrapf(originalErr, "wrapped")
+
+	assert.NotNil(t, originalErr.stack)
+	assert.NotNil(t, wrappedErr.stack)
+
+	// When wrapping an existing ErrWrap, it should preserve the original stack
+	assert.Equal(t, originalErr.stack, wrappedErr.stack)
+
+	// When wrapping a regular error, it should capture new stack
+	regularErr := errors.New("regular")
+	wrappedRegular := Wrapf(regularErr, "wrapped regular")
+	assert.NotNil(t, wrappedRegular.stack)
+}
+
+func TestNotModifiedError(t *testing.T) {
+	headers := make(http.Header)
+	headers.Set("Cache-Control", "no-cache")
+	headers.Set("ETag", `"abc123"`)
+	headers.Set("Last-Modified", "Wed, 21 Oct 2015 07:28:00 GMT")
+
+	originalErr := newNotModifiedError(headers)
+	wrappedErr := Wrapf(originalErr, "cache validation failed for resource %s", "/api/users/123")
+
+	assert.Equal(t, "not modified: cache validation failed for resource /api/users/123", wrappedErr.Error())
+	assert.Equal(t, originalErr, wrappedErr.Unwrap())
+
+	require.ErrorIs(t, wrappedErr, originalErr)
+
+	differentHeaders := make(http.Header)
+	differentHeaders.Set("Cache-Control", "public")
+	differentErr := newNotModifiedError(differentHeaders)
+	require.NotErrorIs(t, wrappedErr, differentErr)
+
+	var extractedNotModified notModifiedError
+	require.ErrorAs(t, wrappedErr, &extractedNotModified)
+
+	extractedHeaders := extractedNotModified.Headers()
+	assert.Equal(t, "no-cache", extractedHeaders.Get("Cache-Control"))
+	assert.Equal(t, `"abc123"`, extractedHeaders.Get("ETag"))
+	assert.Equal(t, "Wed, 21 Oct 2015 07:28:00 GMT", extractedHeaders.Get("Last-Modified"))
+}
+
+func TestWrapStdErr(t *testing.T) {
+	err := Wrap(context.DeadlineExceeded)
+	assert.ErrorIs(t, err, context.DeadlineExceeded)
+}

+ 18 - 111
ierrors/errors.go

@@ -1,85 +1,13 @@
 package ierrors
 package ierrors
 
 
 import (
 import (
-	"fmt"
-	"net/http"
-	"runtime"
-	"strings"
+	"github.com/imgproxy/imgproxy/v3/errwrap"
 )
 )
 
 
-type Option func(*Error)
-
-type Error struct {
-	err error
-
-	prefix        string
-	statusCode    int
-	publicMessage string
-	shouldReport  bool
-
-	stack []uintptr
-}
-
-func (e *Error) Error() string {
-	if len(e.prefix) > 0 {
-		return fmt.Sprintf("%s: %s", e.prefix, e.err.Error())
-	}
-
-	return e.err.Error()
-}
-
-func (e *Error) Unwrap() error {
-	return e.err
-}
-
-func (e *Error) Cause() error {
-	return e.err
-}
-
-func (e *Error) StatusCode() int {
-	if e.statusCode <= 0 {
-		return http.StatusInternalServerError
-	}
-
-	return e.statusCode
-}
-
-func (e *Error) PublicMessage() string {
-	if len(e.publicMessage) == 0 {
-		return "Internal error"
-	}
-
-	return e.publicMessage
-}
-
-func (e *Error) ShouldReport() bool {
-	return e.shouldReport
-}
-
-func (e *Error) StackTrace() []uintptr {
-	return e.stack
-}
-
-func (e *Error) Callers() []uintptr {
-	return e.stack
-}
-
-func (e *Error) FormatStackLines() []string {
-	lines := make([]string, len(e.stack))
-
-	for i, pc := range e.stack {
-		f := runtime.FuncForPC(pc)
-		file, line := f.FileLine(pc)
-		lines[i] = fmt.Sprintf("%s:%d %s", file, line, f.Name())
-	}
-
-	return lines
-}
-
-func (e *Error) FormatStack() string {
-	return strings.Join(e.FormatStackLines(), "\n")
-}
+type Error = errwrap.ErrWrap
+type Option func(*Error) *Error
 
 
+// Now, it's fallback to the original Wrap function
 func Wrap(err error, stackSkip int, opts ...Option) *Error {
 func Wrap(err error, stackSkip int, opts ...Option) *Error {
 	if err == nil {
 	if err == nil {
 		return nil
 		return nil
@@ -87,62 +15,41 @@ func Wrap(err error, stackSkip int, opts ...Option) *Error {
 
 
 	var e *Error
 	var e *Error
 
 
-	if ierr, ok := err.(*Error); ok {
-		// if we have some options, we need to copy the error to not modify the original one
-		if len(opts) > 0 {
-			ecopy := *ierr
-			e = &ecopy
-		} else {
-			return ierr
-		}
+	x, ok := err.(*Error)
+	if ok {
+		e = errwrap.Wrap(x)
 	} else {
 	} else {
-		e = &Error{
-			err:          err,
-			shouldReport: true,
-		}
+		e = errwrap.From(err, stackSkip)
 	}
 	}
 
 
 	for _, opt := range opts {
 	for _, opt := range opts {
-		opt(e)
-	}
-
-	if len(e.stack) == 0 {
-		e.stack = callers(stackSkip + 1)
+		e = opt(e)
 	}
 	}
 
 
 	return e
 	return e
 }
 }
 
 
 func WithStatusCode(code int) Option {
 func WithStatusCode(code int) Option {
-	return func(e *Error) {
-		e.statusCode = code
+	return func(e *Error) *Error {
+		x := e.WithStatusCode(code)
+		return x
 	}
 	}
 }
 }
 
 
 func WithPublicMessage(msg string) Option {
 func WithPublicMessage(msg string) Option {
-	return func(e *Error) {
-		e.publicMessage = msg
+	return func(e *Error) *Error {
+		return e.WithPublicMessage(msg)
 	}
 	}
 }
 }
 
 
 func WithPrefix(prefix string) Option {
 func WithPrefix(prefix string) Option {
-	return func(e *Error) {
-		if len(e.prefix) > 0 {
-			e.prefix = fmt.Sprintf("%s: %s", prefix, e.prefix)
-		} else {
-			e.prefix = prefix
-		}
+	return func(e *Error) *Error {
+		return errwrap.Wrapf(e, "%s", prefix)
 	}
 	}
 }
 }
 
 
 func WithShouldReport(report bool) Option {
 func WithShouldReport(report bool) Option {
-	return func(e *Error) {
-		e.shouldReport = report
+	return func(e *Error) *Error {
+		return e.WithShouldReport(report)
 	}
 	}
 }
 }
-
-func callers(skip int) []uintptr {
-	stack := make([]uintptr, 10)
-	n := runtime.Callers(skip+2, stack)
-	return stack[:n]
-}

+ 2 - 1
imagedata/factory.go

@@ -7,6 +7,7 @@ import (
 	"net/http"
 	"net/http"
 	"os"
 	"os"
 
 
+	"github.com/imgproxy/imgproxy/v3/errwrap"
 	"github.com/imgproxy/imgproxy/v3/imagemeta"
 	"github.com/imgproxy/imgproxy/v3/imagemeta"
 	"github.com/imgproxy/imgproxy/v3/imagetype"
 	"github.com/imgproxy/imgproxy/v3/imagetype"
 	"github.com/imgproxy/imgproxy/v3/security"
 	"github.com/imgproxy/imgproxy/v3/security"
@@ -82,7 +83,7 @@ func NewFromPath(path string, secopts security.Options) (ImageData, error) {
 func NewFromBase64(encoded string, secopts security.Options) (ImageData, error) {
 func NewFromBase64(encoded string, secopts security.Options) (ImageData, error) {
 	b, err := base64.StdEncoding.DecodeString(encoded)
 	b, err := base64.StdEncoding.DecodeString(encoded)
 	if err != nil {
 	if err != nil {
-		return nil, err
+		return nil, errwrap.Wrapf(err, "failed to decode base64 string")
 	}
 	}
 
 
 	r := bytes.NewReader(b)
 	r := bytes.NewReader(b)

+ 11 - 22
imagedata/image_data.go

@@ -9,6 +9,7 @@ import (
 	"sync"
 	"sync"
 
 
 	"github.com/imgproxy/imgproxy/v3/config"
 	"github.com/imgproxy/imgproxy/v3/config"
+	"github.com/imgproxy/imgproxy/v3/errwrap"
 	"github.com/imgproxy/imgproxy/v3/ierrors"
 	"github.com/imgproxy/imgproxy/v3/ierrors"
 	"github.com/imgproxy/imgproxy/v3/imagetype"
 	"github.com/imgproxy/imgproxy/v3/imagetype"
 	"github.com/imgproxy/imgproxy/v3/security"
 	"github.com/imgproxy/imgproxy/v3/security"
@@ -99,29 +100,20 @@ func loadWatermark() error {
 	case len(config.WatermarkData) > 0:
 	case len(config.WatermarkData) > 0:
 		Watermark, err = NewFromBase64(config.WatermarkData, security.DefaultOptions())
 		Watermark, err = NewFromBase64(config.WatermarkData, security.DefaultOptions())
 
 
-		// NOTE: this should be something like err = ierrors.Wrap(err).WithStackDeep(0).WithPrefix("watermark")
-		// In the NewFromBase64 all errors should be wrapped to something like
-		// .WithPrefix("load from base64")
-		if err != nil {
-			return ierrors.Wrap(err, 0, ierrors.WithPrefix("can't load watermark from Base64"))
-		}
-
 	case len(config.WatermarkPath) > 0:
 	case len(config.WatermarkPath) > 0:
 		Watermark, err = NewFromPath(config.WatermarkPath, security.DefaultOptions())
 		Watermark, err = NewFromPath(config.WatermarkPath, security.DefaultOptions())
-		if err != nil {
-			return ierrors.Wrap(err, 0, ierrors.WithPrefix("can't read watermark from file"))
-		}
 
 
 	case len(config.WatermarkURL) > 0:
 	case len(config.WatermarkURL) > 0:
 		Watermark, err = Download(context.Background(), config.WatermarkURL, "watermark", DownloadOptions{Header: nil, CookieJar: nil}, security.DefaultOptions())
 		Watermark, err = Download(context.Background(), config.WatermarkURL, "watermark", DownloadOptions{Header: nil, CookieJar: nil}, security.DefaultOptions())
-		if err != nil {
-			return ierrors.Wrap(err, 0, ierrors.WithPrefix("can't download from URL"))
-		}
 
 
 	default:
 	default:
 		Watermark = nil
 		Watermark = nil
 	}
 	}
 
 
+	if err != nil {
+		return errwrap.Wrapf(err, "failed to load watermark")
+	}
+
 	return nil
 	return nil
 }
 }
 
 
@@ -129,21 +121,12 @@ func loadFallbackImage() (err error) {
 	switch {
 	switch {
 	case len(config.FallbackImageData) > 0:
 	case len(config.FallbackImageData) > 0:
 		FallbackImage, err = NewFromBase64(config.FallbackImageData, security.DefaultOptions())
 		FallbackImage, err = NewFromBase64(config.FallbackImageData, security.DefaultOptions())
-		if err != nil {
-			return ierrors.Wrap(err, 0, ierrors.WithPrefix("can't load fallback image from Base64"))
-		}
 
 
 	case len(config.FallbackImagePath) > 0:
 	case len(config.FallbackImagePath) > 0:
 		FallbackImage, err = NewFromPath(config.FallbackImagePath, security.DefaultOptions())
 		FallbackImage, err = NewFromPath(config.FallbackImagePath, security.DefaultOptions())
-		if err != nil {
-			return ierrors.Wrap(err, 0, ierrors.WithPrefix("can't read fallback image from file"))
-		}
 
 
 	case len(config.FallbackImageURL) > 0:
 	case len(config.FallbackImageURL) > 0:
 		FallbackImage, err = Download(context.Background(), config.FallbackImageURL, "fallback image", DownloadOptions{Header: nil, CookieJar: nil}, security.DefaultOptions())
 		FallbackImage, err = Download(context.Background(), config.FallbackImageURL, "fallback image", DownloadOptions{Header: nil, CookieJar: nil}, security.DefaultOptions())
-		if err != nil {
-			return ierrors.Wrap(err, 0, ierrors.WithPrefix("can't download from URL"))
-		}
 
 
 	default:
 	default:
 		FallbackImage = nil
 		FallbackImage = nil
@@ -153,6 +136,12 @@ func loadFallbackImage() (err error) {
 		FallbackImage.Headers().Set("Fallback-Image", "1")
 		FallbackImage.Headers().Set("Fallback-Image", "1")
 	}
 	}
 
 
+	// Otherwise, it triggers false positive lint error in Init()
+	// TODO: fix this eventually
+	if err != nil {
+		err = errwrap.Wrapf(err, "failed to load fallback image")
+	}
+
 	return err
 	return err
 }
 }
 
 

+ 4 - 4
imagedata/read.go

@@ -8,7 +8,7 @@ import (
 	"github.com/imgproxy/imgproxy/v3/bufpool"
 	"github.com/imgproxy/imgproxy/v3/bufpool"
 	"github.com/imgproxy/imgproxy/v3/bufreader"
 	"github.com/imgproxy/imgproxy/v3/bufreader"
 	"github.com/imgproxy/imgproxy/v3/config"
 	"github.com/imgproxy/imgproxy/v3/config"
-	"github.com/imgproxy/imgproxy/v3/imagefetcher"
+	"github.com/imgproxy/imgproxy/v3/errwrap"
 	"github.com/imgproxy/imgproxy/v3/imagemeta"
 	"github.com/imgproxy/imgproxy/v3/imagemeta"
 	"github.com/imgproxy/imgproxy/v3/security"
 	"github.com/imgproxy/imgproxy/v3/security"
 )
 )
@@ -30,14 +30,14 @@ func readAndCheckImage(r io.Reader, contentLength int, secopts security.Options)
 		buf.Reset()
 		buf.Reset()
 		cancel()
 		cancel()
 
 
-		return nil, imagefetcher.WrapError(err)
+		return nil, errwrap.Wrap(err)
 	}
 	}
 
 
 	if err = security.CheckDimensions(meta.Width(), meta.Height(), 1, secopts); err != nil {
 	if err = security.CheckDimensions(meta.Width(), meta.Height(), 1, secopts); err != nil {
 		buf.Reset()
 		buf.Reset()
 		cancel()
 		cancel()
 
 
-		return nil, imagefetcher.WrapError(err)
+		return nil, errwrap.Wrap(err)
 	}
 	}
 
 
 	downloadBufPool.GrowBuffer(buf, contentLength)
 	downloadBufPool.GrowBuffer(buf, contentLength)
@@ -46,7 +46,7 @@ func readAndCheckImage(r io.Reader, contentLength int, secopts security.Options)
 		buf.Reset()
 		buf.Reset()
 		cancel()
 		cancel()
 
 
-		return nil, imagefetcher.WrapError(err)
+		return nil, errwrap.Wrap(err)
 	}
 	}
 
 
 	i := NewFromBytesWithFormat(meta.Format(), buf.Bytes(), nil)
 	i := NewFromBytesWithFormat(meta.Format(), buf.Bytes(), nil)

+ 91 - 114
imagefetcher/errors.go

@@ -5,156 +5,135 @@ import (
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
 	"net/http"
 	"net/http"
+	"reflect"
 
 
-	"github.com/imgproxy/imgproxy/v3/ierrors"
+	"github.com/imgproxy/imgproxy/v3/errwrap"
 	"github.com/imgproxy/imgproxy/v3/security"
 	"github.com/imgproxy/imgproxy/v3/security"
 )
 )
 
 
 const msgSourceImageIsUnreachable = "Source image is unreachable"
 const msgSourceImageIsUnreachable = "Source image is unreachable"
 
 
 type (
 type (
-	ImageRequestError          struct{ error }
-	ImageRequstSchemeError     string
-	ImagePartialResponseError  string
-	ImageResponseStatusError   string
-	ImageTooManyRedirectsError string
-	ImageRequestCanceledError  struct{ error }
-	ImageRequestTimeoutError   struct{ error }
-
-	NotModifiedError struct {
-		headers http.Header
-	}
-
-	httpError interface {
-		Timeout() bool
-	}
+	RequestError          struct{ error }
+	RequestSchemeError    struct{ error }
+	PartialResponseError  struct{ error }
+	ResponseStatusError   struct{ error }
+	TooManyRedirectsError struct{ error }
+	RequestCanceledError  struct{ error }
+	RequestTimeoutError   struct{ error }
 )
 )
 
 
-func newImageRequestError(err error) error {
-	return ierrors.Wrap(
-		ImageRequestError{err},
-		1,
-		ierrors.WithStatusCode(http.StatusNotFound),
-		ierrors.WithPublicMessage(msgSourceImageIsUnreachable),
-		ierrors.WithShouldReport(false),
-	)
+type NotModifiedError struct {
+	headers http.Header
 }
 }
 
 
-func (e ImageRequestError) Unwrap() error {
-	return e.error
+type httpError interface {
+	Timeout() bool
 }
 }
 
 
-func newImageRequstSchemeError(scheme string) error {
-	return ierrors.Wrap(
-		ImageRequstSchemeError(fmt.Sprintf("Unknown scheme: %s", scheme)),
-		1,
-		ierrors.WithStatusCode(http.StatusNotFound),
-		ierrors.WithPublicMessage(msgSourceImageIsUnreachable),
-		ierrors.WithShouldReport(false),
-	)
+func newRequestError(err error) error {
+	return errwrap.From(
+		RequestError{err}, 1,
+	).
+		WithStatusCode(http.StatusNotFound).
+		WithPublicMessage(msgSourceImageIsUnreachable).
+		WithShouldReport(false)
 }
 }
 
 
-func (e ImageRequstSchemeError) Error() string { return string(e) }
-
-func newImagePartialResponseError(msg string) error {
-	return ierrors.Wrap(
-		ImagePartialResponseError(msg),
-		1,
-		ierrors.WithStatusCode(http.StatusNotFound),
-		ierrors.WithPublicMessage(msgSourceImageIsUnreachable),
-		ierrors.WithShouldReport(false),
-	)
+func newRequestSchemeError(scheme string) error {
+	return errwrap.From(
+		RequestSchemeError{fmt.Errorf("unknown scheme: %s", scheme)}, 1,
+	).
+		WithStatusCode(http.StatusNotFound).
+		WithPublicMessage(msgSourceImageIsUnreachable).
+		WithShouldReport(false)
 }
 }
 
 
-func (e ImagePartialResponseError) Error() string { return string(e) }
+func newPartialResponseError(msg string) error {
+	return errwrap.From(
+		PartialResponseError{errors.New(msg)}, 1,
+	).
+		WithStatusCode(http.StatusNotFound).
+		WithPublicMessage(msgSourceImageIsUnreachable).
+		WithShouldReport(false)
+}
 
 
-func newImageResponseStatusError(status int, body string) error {
-	var msg string
+func newResponseStatusError(status int, body string) error {
+	var err error
 
 
 	if len(body) > 0 {
 	if len(body) > 0 {
-		msg = fmt.Sprintf("Status: %d; %s", status, body)
+		err = fmt.Errorf("status: %d; %s", status, body)
 	} else {
 	} else {
-		msg = fmt.Sprintf("Status: %d", status)
+		err = fmt.Errorf("status: %d", status)
 	}
 	}
 
 
-	statusCode := 404
+	statusCode := http.StatusNotFound
 	if status >= 500 {
 	if status >= 500 {
-		statusCode = 500
+		statusCode = http.StatusInternalServerError
 	}
 	}
 
 
-	return ierrors.Wrap(
-		ImageResponseStatusError(msg),
-		1,
-		ierrors.WithStatusCode(statusCode),
-		ierrors.WithPublicMessage(msgSourceImageIsUnreachable),
-		ierrors.WithShouldReport(false),
-	)
+	return errwrap.From(
+		ResponseStatusError{err}, 1,
+	).
+		WithStatusCode(statusCode).
+		WithPublicMessage(msgSourceImageIsUnreachable).
+		WithShouldReport(false)
 }
 }
 
 
-func (e ImageResponseStatusError) Error() string { return string(e) }
-
-func newImageTooManyRedirectsError(n int) error {
-	return ierrors.Wrap(
-		ImageTooManyRedirectsError(fmt.Sprintf("Stopped after %d redirects", n)),
-		1,
-		ierrors.WithStatusCode(http.StatusNotFound),
-		ierrors.WithPublicMessage(msgSourceImageIsUnreachable),
-		ierrors.WithShouldReport(false),
-	)
+func newTooManyRedirectsError(n int) error {
+	return errwrap.From(
+		TooManyRedirectsError{fmt.Errorf("stopped after %d redirects", n)}, 1,
+	).
+		WithStatusCode(http.StatusNotFound).
+		WithPublicMessage(msgSourceImageIsUnreachable).
+		WithShouldReport(false)
 }
 }
 
 
-func (e ImageTooManyRedirectsError) Error() string { return string(e) }
-
-func newImageRequestCanceledError(err error) error {
-	return ierrors.Wrap(
-		ImageRequestCanceledError{err},
-		2,
-		ierrors.WithStatusCode(499),
-		ierrors.WithPublicMessage(msgSourceImageIsUnreachable),
-		ierrors.WithShouldReport(false),
-	)
+func newRequestCanceledError(err error) error {
+	// stack 2
+	return errwrap.Wrapf(
+		RequestCanceledError{err},
+		"the image request is cancelled",
+	).WithStatusCode(499).
+		WithPublicMessage(msgSourceImageIsUnreachable).
+		WithShouldReport(false)
 }
 }
 
 
-func (e ImageRequestCanceledError) Error() string {
-	return fmt.Sprintf("The image request is cancelled: %s", e.error)
-}
-
-func (e ImageRequestCanceledError) Unwrap() error { return e.error }
-
 func newImageRequestTimeoutError(err error) error {
 func newImageRequestTimeoutError(err error) error {
-	return ierrors.Wrap(
-		ImageRequestTimeoutError{err},
+	return errwrap.From(
+		RequestTimeoutError{err},
 		2,
 		2,
-		ierrors.WithStatusCode(http.StatusGatewayTimeout),
-		ierrors.WithPublicMessage(msgSourceImageIsUnreachable),
-		ierrors.WithShouldReport(false),
-	)
+	).
+		WithStatusCode(http.StatusGatewayTimeout).
+		WithPublicMessage(msgSourceImageIsUnreachable).
+		WithShouldReport(false)
 }
 }
 
 
-func (e ImageRequestTimeoutError) Error() string {
-	return fmt.Sprintf("The image request timed out: %s", e.error)
-}
-
-func (e ImageRequestTimeoutError) Unwrap() error { return e.error }
-
 func newNotModifiedError(headers http.Header) error {
 func newNotModifiedError(headers http.Header) error {
-	return ierrors.Wrap(
+	return errwrap.From(
 		NotModifiedError{headers},
 		NotModifiedError{headers},
 		1,
 		1,
-		ierrors.WithStatusCode(http.StatusNotModified),
-		ierrors.WithPublicMessage("Not modified"),
-		ierrors.WithShouldReport(false),
-	)
+	).
+		WithStatusCode(http.StatusNotModified).
+		WithPublicMessage("Not modified").
+		WithShouldReport(false)
 }
 }
 
 
-func (e NotModifiedError) Error() string { return "Not modified" }
+func (e NotModifiedError) Error() string { return "not modified" }
 
 
 func (e NotModifiedError) Headers() http.Header {
 func (e NotModifiedError) Headers() http.Header {
 	return e.headers
 	return e.headers
 }
 }
 
 
-// NOTE: make private when we remove download functions from imagedata package
-func WrapError(err error) error {
+// Is performs comparison of two notModifiedError instances.
+// Any error should be Comparable, http.Header is not comparable,
+// hence, we need to compare headers manually.
+func (nm NotModifiedError) Is(target error) bool {
+	m, ok := target.(NotModifiedError)
+	return ok && reflect.DeepEqual(nm.headers, m.headers)
+}
+
+func wrapError(err error) error {
 	isTimeout := false
 	isTimeout := false
 
 
 	var secArrdErr security.SourceAddressError
 	var secArrdErr security.SourceAddressError
@@ -163,15 +142,12 @@ func WrapError(err error) error {
 	case errors.Is(err, context.DeadlineExceeded):
 	case errors.Is(err, context.DeadlineExceeded):
 		isTimeout = true
 		isTimeout = true
 	case errors.Is(err, context.Canceled):
 	case errors.Is(err, context.Canceled):
-		return newImageRequestCanceledError(err)
+		return newRequestCanceledError(err)
 	case errors.As(err, &secArrdErr):
 	case errors.As(err, &secArrdErr):
-		return ierrors.Wrap(
-			err,
-			1,
-			ierrors.WithStatusCode(404),
-			ierrors.WithPublicMessage(msgSourceImageIsUnreachable),
-			ierrors.WithShouldReport(false),
-		)
+		return errwrap.From(err, 1).
+			WithStatusCode(404).
+			WithPublicMessage(msgSourceImageIsUnreachable).
+			WithShouldReport(false)
 	default:
 	default:
 		if httpErr, ok := err.(httpError); ok {
 		if httpErr, ok := err.(httpError); ok {
 			isTimeout = httpErr.Timeout()
 			isTimeout = httpErr.Timeout()
@@ -179,8 +155,9 @@ func WrapError(err error) error {
 	}
 	}
 
 
 	if isTimeout {
 	if isTimeout {
-		return newImageRequestTimeoutError(err)
+		return errwrap.From(newImageRequestTimeoutError(err), 1)
 	}
 	}
 
 
-	return ierrors.Wrap(err, 1)
+	// shift stack by 1 (NOTE: START WRAPPING FROM ORIGIN)
+	return errwrap.Wrap(err)
 }
 }

+ 4 - 4
imagefetcher/fetcher.go

@@ -33,7 +33,7 @@ func NewFetcher(transport *transport.Transport, maxRedirects int) (*Fetcher, err
 func (f *Fetcher) checkRedirect(req *http.Request, via []*http.Request) error {
 func (f *Fetcher) checkRedirect(req *http.Request, via []*http.Request) error {
 	redirects := len(via)
 	redirects := len(via)
 	if redirects >= f.maxRedirects {
 	if redirects >= f.maxRedirects {
-		return newImageTooManyRedirectsError(redirects)
+		return newTooManyRedirectsError(redirects)
 	}
 	}
 	return nil
 	return nil
 }
 }
@@ -46,7 +46,7 @@ func (f *Fetcher) newHttpClient() *http.Client {
 	}
 	}
 }
 }
 
 
-// NewImageFetcherRequest creates a new ImageFetcherRequest with the provided context, URL, headers, and cookie jar
+// BuildRequest creates a new ImageFetcherRequest with the provided context, URL, headers, and cookie jar
 func (f *Fetcher) BuildRequest(ctx context.Context, url string, header http.Header, jar http.CookieJar) (*Request, error) {
 func (f *Fetcher) BuildRequest(ctx context.Context, url string, header http.Header, jar http.CookieJar) (*Request, error) {
 	url = common.EscapeURL(url)
 	url = common.EscapeURL(url)
 
 
@@ -56,13 +56,13 @@ func (f *Fetcher) BuildRequest(ctx context.Context, url string, header http.Head
 	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
 	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
 	if err != nil {
 	if err != nil {
 		cancel()
 		cancel()
-		return nil, newImageRequestError(err)
+		return nil, newRequestError(err)
 	}
 	}
 
 
 	// Check if the URL scheme is supported
 	// Check if the URL scheme is supported
 	if !f.transport.IsProtocolRegistered(req.URL.Scheme) {
 	if !f.transport.IsProtocolRegistered(req.URL.Scheme) {
 		cancel()
 		cancel()
-		return nil, newImageRequstSchemeError(req.URL.Scheme)
+		return nil, newRequestSchemeError(req.URL.Scheme)
 	}
 	}
 
 
 	// Add cookies from the jar to the request (if any)
 	// Add cookies from the jar to the request (if any)

+ 5 - 5
imagefetcher/request.go

@@ -63,7 +63,7 @@ func (r *Request) Send() (*http.Response, error) {
 			}
 			}
 		}
 		}
 
 
-		return nil, WrapError(err)
+		return nil, wrapError(err)
 	}
 	}
 }
 }
 
 
@@ -99,7 +99,7 @@ func (r *Request) FetchImage() (*http.Response, error) {
 	} else if res.StatusCode != http.StatusOK {
 	} else if res.StatusCode != http.StatusOK {
 		body := extractErraticBody(res)
 		body := extractErraticBody(res)
 		cancel()
 		cancel()
-		return nil, newImageResponseStatusError(res.StatusCode, body)
+		return nil, newResponseStatusError(res.StatusCode, body)
 	}
 	}
 
 
 	// If the response is gzip encoded, wrap it in a gzip reader
 	// If the response is gzip encoded, wrap it in a gzip reader
@@ -136,11 +136,11 @@ func checkPartialContentResponse(res *http.Response) error {
 	rangeParts := contentRangeRe.FindStringSubmatch(contentRange)
 	rangeParts := contentRangeRe.FindStringSubmatch(contentRange)
 
 
 	if len(rangeParts) == 0 {
 	if len(rangeParts) == 0 {
-		return newImagePartialResponseError("Partial response with invalid Content-Range header")
+		return newPartialResponseError("Partial response with invalid Content-Range header")
 	}
 	}
 
 
 	if rangeParts[1] == "*" || rangeParts[2] != "0" {
 	if rangeParts[1] == "*" || rangeParts[2] != "0" {
-		return newImagePartialResponseError("Partial response with incomplete content")
+		return newPartialResponseError("Partial response with incomplete content")
 	}
 	}
 
 
 	contentLengthStr := rangeParts[4]
 	contentLengthStr := rangeParts[4]
@@ -152,7 +152,7 @@ func checkPartialContentResponse(res *http.Response) error {
 	rangeEnd, _ := strconv.Atoi(rangeParts[3])
 	rangeEnd, _ := strconv.Atoi(rangeParts[3])
 
 
 	if contentLength <= 0 || rangeEnd != contentLength-1 {
 	if contentLength <= 0 || rangeEnd != contentLength-1 {
-		return newImagePartialResponseError("Partial response with incomplete content")
+		return newPartialResponseError("Partial response with incomplete content")
 	}
 	}
 
 
 	return nil
 	return nil

+ 2 - 2
security/image_size.go

@@ -10,11 +10,11 @@ func CheckDimensions(width, height, frames int, opts Options) error {
 
 
 	if frames > 1 && opts.MaxAnimationFrameResolution > 0 {
 	if frames > 1 && opts.MaxAnimationFrameResolution > 0 {
 		if width*height > opts.MaxAnimationFrameResolution {
 		if width*height > opts.MaxAnimationFrameResolution {
-			return newImageResolutionError("Source image frame resolution is too big")
+			return newImageResolutionError("source image frame resolution is too big")
 		}
 		}
 	} else {
 	} else {
 		if width*height*frames > opts.MaxSrcResolution {
 		if width*height*frames > opts.MaxSrcResolution {
-			return newImageResolutionError("Source image resolution is too big")
+			return newImageResolutionError("source image resolution is too big")
 		}
 		}
 	}
 	}