diff --git a/cookies/cookies.go b/cookies/cookies.go index eb81ee4c..98095c47 100644 --- a/cookies/cookies.go +++ b/cookies/cookies.go @@ -10,8 +10,13 @@ import ( "golang.org/x/net/publicsuffix" "github.com/imgproxy/imgproxy/v3/config" + "github.com/imgproxy/imgproxy/v3/ierrors" ) +type cookieError string + +func (e cookieError) Error() string { return string(e) } + type anyCookieJarEntry struct { Name string Value string @@ -75,7 +80,7 @@ func JarFromRequest(r *http.Request) (jar http.CookieJar, err error) { if !config.CookiePassthroughAll { if len(config.CookieBaseURL) > 0 { if cookieBase, err = url.Parse(config.CookieBaseURL); err != nil { - return nil, fmt.Errorf("can't parse cookie base URL: %s", err) + return nil, ierrors.Wrap(cookieError(fmt.Sprintf("can't parse cookie base URL: %s", err)), 0) } } diff --git a/imagedata/download.go b/imagedata/download.go index 752ac185..d955d76e 100644 --- a/imagedata/download.go +++ b/imagedata/download.go @@ -3,7 +3,6 @@ package imagedata import ( "compress/gzip" "context" - "fmt" "io" "net/http" "net/http/cookiejar" @@ -105,7 +104,7 @@ func initDownloading() error { CheckRedirect: func(req *http.Request, via []*http.Request) error { redirects := len(via) if redirects >= config.MaxRedirects { - return fmt.Errorf("stopped after %d redirects", redirects) + return newImageTooManyRedirectsError(redirects) } return nil }, diff --git a/imagedata/errors.go b/imagedata/errors.go index e6ac5ed7..c5e569b0 100644 --- a/imagedata/errors.go +++ b/imagedata/errors.go @@ -11,12 +11,13 @@ import ( ) type ( - ImageRequestError struct{ error } - ImageRequstSchemeError string - ImagePartialResponseError string - ImageResponseStatusError string - ImageRequestCanceledError struct{ error } - ImageRequestTimeoutError struct{ error } + ImageRequestError struct{ error } + ImageRequstSchemeError string + ImagePartialResponseError string + ImageResponseStatusError string + ImageTooManyRedirectsError string + ImageRequestCanceledError struct{ error } + ImageRequestTimeoutError struct{ error } NotModifiedError struct { headers map[string]string @@ -90,6 +91,18 @@ func newImageResponseStatusError(status int, body string) error { 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 (e ImageTooManyRedirectsError) Error() string { return string(e) } + func newImageRequestCanceledError(err error) error { return ierrors.Wrap( ImageRequestCanceledError{err}, diff --git a/imagemeta/heif.go b/imagemeta/heif.go index ad422fac..86a5a191 100644 --- a/imagemeta/heif.go +++ b/imagemeta/heif.go @@ -185,7 +185,7 @@ func heifReadHldr(r io.Reader, boxDataSize uint64) error { } if !bytes.Equal(data[8:12], heifPict) { - return fmt.Errorf("Invalid handler. Expected: pict, actual: %s", data[8:12]) + return newFormatError("HEIF", fmt.Sprintf("Invalid handler. Expected: pict, actual: %s", data[8:12])) } return nil diff --git a/options/url.go b/options/url.go index c1f229c1..6c01ee92 100644 --- a/options/url.go +++ b/options/url.go @@ -2,7 +2,6 @@ package options import ( "encoding/base64" - "errors" "fmt" "net/url" "strings" @@ -35,11 +34,11 @@ func decodeBase64URL(parts []string) (string, string, error) { urlParts := strings.Split(encoded, ".") if len(urlParts[0]) == 0 { - return "", "", errors.New("Image URL is empty") + return "", "", newInvalidURLError("Image URL is empty") } if len(urlParts) > 2 { - return "", "", fmt.Errorf("Multiple formats are specified: %s", encoded) + return "", "", newInvalidURLError("Multiple formats are specified: %s", encoded) } if len(urlParts) == 2 && len(urlParts[1]) > 0 { @@ -48,7 +47,7 @@ func decodeBase64URL(parts []string) (string, string, error) { imageURL, err := base64.RawURLEncoding.DecodeString(strings.TrimRight(urlParts[0], "=")) if err != nil { - return "", "", fmt.Errorf("Invalid url encoding: %s", encoded) + return "", "", newInvalidURLError("Invalid url encoding: %s", encoded) } return preprocessURL(string(imageURL)), format, nil @@ -61,11 +60,11 @@ func decodePlainURL(parts []string) (string, string, error) { urlParts := strings.Split(encoded, "@") if len(urlParts[0]) == 0 { - return "", "", errors.New("Image URL is empty") + return "", "", newInvalidURLError("Image URL is empty") } if len(urlParts) > 2 { - return "", "", fmt.Errorf("Multiple formats are specified: %s", encoded) + return "", "", newInvalidURLError("Multiple formats are specified: %s", encoded) } if len(urlParts) == 2 && len(urlParts[1]) > 0 { @@ -74,7 +73,7 @@ func decodePlainURL(parts []string) (string, string, error) { unescaped, err := url.PathUnescape(urlParts[0]) if err != nil { - return "", "", fmt.Errorf("Invalid url encoding: %s", encoded) + return "", "", newInvalidURLError("Invalid url encoding: %s", encoded) } return preprocessURL(unescaped), format, nil @@ -82,7 +81,7 @@ func decodePlainURL(parts []string) (string, string, error) { func DecodeURL(parts []string) (string, string, error) { if len(parts) == 0 { - return "", "", errors.New("Image URL is empty") + return "", "", newInvalidURLError("Image URL is empty") } if parts[0] == urlTokenPlain && len(parts) > 1 { diff --git a/processing/errors.go b/processing/errors.go new file mode 100644 index 00000000..37e7a82c --- /dev/null +++ b/processing/errors.go @@ -0,0 +1,25 @@ +package processing + +import ( + "fmt" + "net/http" + + "github.com/imgproxy/imgproxy/v3/ierrors" + "github.com/imgproxy/imgproxy/v3/imagetype" +) + +type ( + SaveFormatError string +) + +func newSaveFormatError(format imagetype.Type) error { + return ierrors.Wrap( + SaveFormatError(fmt.Sprintf("Can't save %s, probably not supported by your libvips", format)), + 1, + ierrors.WithStatusCode(http.StatusUnprocessableEntity), + ierrors.WithPublicMessage("Invalid URL"), + ierrors.WithShouldReport(false), + ) +} + +func (e SaveFormatError) Error() string { return string(e) } diff --git a/processing/processing.go b/processing/processing.go index 54bdc658..d15249a6 100644 --- a/processing/processing.go +++ b/processing/processing.go @@ -3,7 +3,6 @@ package processing import ( "context" "errors" - "fmt" "runtime" "strconv" @@ -301,7 +300,7 @@ func ProcessImage(ctx context.Context, imgdata *imagedata.ImageData, po *options } if !vips.SupportsSave(po.Format) { - return nil, fmt.Errorf("Can't save %s, probably not supported by your libvips", po.Format) + return nil, newSaveFormatError(po.Format) } if po.Format.SupportsAnimationSave() && animated { diff --git a/transport/azure/azure.go b/transport/azure/azure.go index 97b9c60b..63ed04c5 100644 --- a/transport/azure/azure.go +++ b/transport/azure/azure.go @@ -109,7 +109,7 @@ func (t transport) RoundTrip(req *http.Request) (*http.Response, error) { if r := req.Header.Get("Range"); len(r) != 0 { start, end, err := httprange.Parse(r) if err != nil { - return httprange.InvalidHTTPRangeResponse(req), err + return httprange.InvalidHTTPRangeResponse(req), nil } if end != 0 { diff --git a/transport/gcs/gcs.go b/transport/gcs/gcs.go index 2988ee13..2fb71330 100644 --- a/transport/gcs/gcs.go +++ b/transport/gcs/gcs.go @@ -16,6 +16,7 @@ import ( "github.com/imgproxy/imgproxy/v3/config" "github.com/imgproxy/imgproxy/v3/httprange" + "github.com/imgproxy/imgproxy/v3/ierrors" defaultTransport "github.com/imgproxy/imgproxy/v3/transport" "github.com/imgproxy/imgproxy/v3/transport/common" "github.com/imgproxy/imgproxy/v3/transport/notmodified" @@ -71,7 +72,7 @@ func New() (http.RoundTripper, error) { client, err = storage.NewClient(context.Background(), opts...) if err != nil { - return nil, fmt.Errorf("Can't create GCS client: %s", err) + return nil, ierrors.Wrap(err, 0, ierrors.WithPrefix("Can't create GCS client")) } return transport{client}, nil diff --git a/transport/s3/s3.go b/transport/s3/s3.go index 90b05e36..3d9e717b 100644 --- a/transport/s3/s3.go +++ b/transport/s3/s3.go @@ -3,7 +3,6 @@ package s3 import ( "context" "errors" - "fmt" "io" "net/http" "strconv" @@ -23,6 +22,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/sts" "github.com/imgproxy/imgproxy/v3/config" + "github.com/imgproxy/imgproxy/v3/ierrors" defaultTransport "github.com/imgproxy/imgproxy/v3/transport" "github.com/imgproxy/imgproxy/v3/transport/common" ) @@ -48,7 +48,7 @@ type transport struct { func New() (http.RoundTripper, error) { conf, err := awsConfig.LoadDefaultConfig(context.Background()) if err != nil { - return nil, fmt.Errorf("can't load AWS S3 config: %s", err) + return nil, ierrors.Wrap(err, 0, ierrors.WithPrefix("can't load AWS S3 config")) } trans, err := defaultTransport.New(false) @@ -90,7 +90,7 @@ func New() (http.RoundTripper, error) { client, err := createClient(conf, clientOptions) if err != nil { - return nil, fmt.Errorf("can't create S3 client: %s", err) + return nil, ierrors.Wrap(err, 0, ierrors.WithPrefix("can't create S3 client")) } return &transport{ @@ -248,7 +248,7 @@ func (t *transport) getClient(ctx context.Context, bucket string) (s3Client, err region, err := s3Manager.GetBucketRegion(ctx, t.defaultClient, bucket) if err != nil { - return nil, fmt.Errorf("can't get bucket region: %s", err) + return nil, ierrors.Wrap(err, 0, ierrors.WithPrefix("can't get bucket region")) } if len(region) == 0 { @@ -265,7 +265,7 @@ func (t *transport) getClient(ctx context.Context, bucket string) (s3Client, err client, err = createClient(conf, t.clientOptions) if err != nil { - return nil, fmt.Errorf("can't create regional S3 client: %s", err) + return nil, ierrors.Wrap(err, 0, ierrors.WithPrefix("can't create regional S3 client")) } t.clientsByRegion[region] = client @@ -295,11 +295,11 @@ func createClient(conf aws.Config, opts []func(*s3.Options)) (s3Client, error) { func handleError(req *http.Request, err error) (*http.Response, error) { var rerr *awsHttp.ResponseError if !errors.As(err, &rerr) { - return nil, err + return nil, ierrors.Wrap(err, 0) } if rerr.Response == nil || rerr.Response.StatusCode < 100 || rerr.Response.StatusCode == 301 { - return nil, err + return nil, ierrors.Wrap(err, 0) } body := strings.NewReader(err.Error()) diff --git a/transport/swift/swift.go b/transport/swift/swift.go index e2c26fe1..151eebad 100644 --- a/transport/swift/swift.go +++ b/transport/swift/swift.go @@ -3,7 +3,6 @@ package swift import ( "context" "errors" - "fmt" "io" "net/http" "strings" @@ -12,6 +11,7 @@ import ( "github.com/ncw/swift/v2" "github.com/imgproxy/imgproxy/v3/config" + "github.com/imgproxy/imgproxy/v3/ierrors" defaultTransport "github.com/imgproxy/imgproxy/v3/transport" "github.com/imgproxy/imgproxy/v3/transport/common" "github.com/imgproxy/imgproxy/v3/transport/notmodified" @@ -44,7 +44,7 @@ func New() (http.RoundTripper, error) { err = c.Authenticate(ctx) if err != nil { - return nil, fmt.Errorf("swift authentication error: %s", err) + return nil, ierrors.Wrap(err, 0, ierrors.WithPrefix("swift authentication error")) } return transport{con: c}, nil @@ -91,7 +91,7 @@ func (t transport) RoundTrip(req *http.Request) (resp *http.Response, err error) }, nil } - return nil, fmt.Errorf("error opening object: %v", err) + return nil, ierrors.Wrap(err, 0, ierrors.WithPrefix("error opening object")) } if config.ETagEnabled { diff --git a/vips/bmp.go b/vips/bmp.go index 08de3c8d..2c702f9f 100644 --- a/vips/bmp.go +++ b/vips/bmp.go @@ -9,7 +9,6 @@ import "C" import ( "bytes" "encoding/binary" - "errors" "io" "unsafe" @@ -36,10 +35,6 @@ type bmpHeader struct { colorImportant uint32 } -// errBmpUnsupported means that the input BMP image uses a valid but unsupported -// feature. -var errBmpUnsupported = errors.New("unsupported BMP image") - func readUint16(b []byte) uint16 { return uint16(b[0]) | uint16(b[1])<<8 } @@ -264,7 +259,7 @@ Loop: // If topDown is false, the image rows will be read bottom-up. func (img *Image) decodeBmpRGB(r io.Reader, width, height, bands int, topDown, noAlpha bool) error { if bands != 3 && bands != 4 { - return errBmpUnsupported + return newVipsError("unsupported BMP image") } imgBands := 3 @@ -381,13 +376,13 @@ func (img *Image) loadBmp(data []byte, noAlpha bool) error { } if string(b[:2]) != "BM" { - return errors.New("not a BMP image") + return newVipsError("not a BMP image") } offset := readUint32(b[10:14]) infoLen := readUint32(b[14:18]) if infoLen != infoHeaderLen && infoLen != v4InfoHeaderLen && infoLen != v5InfoHeaderLen { - return errBmpUnsupported + return newVipsError("unsupported BMP image") } if _, err := io.ReadFull(r, b[fileHeaderLen+4:fileHeaderLen+infoLen]); err != nil { @@ -405,14 +400,14 @@ func (img *Image) loadBmp(data []byte, noAlpha bool) error { height, topDown = -height, true } if width <= 0 || height <= 0 { - return errBmpUnsupported + return newVipsError("unsupported BMP image") } // We only support 1 plane and 8, 24 or 32 bits per pixel planes, bpp, compression := readUint16(b[26:28]), readUint16(b[28:30]), readUint32(b[30:34]) if planes != 1 { - return errBmpUnsupported + return newVipsError("unsupported BMP image") } rle := false @@ -447,10 +442,10 @@ func (img *Image) loadBmp(data []byte, noAlpha bool) error { case bpp == 32 && rmask == 0xff0000 && gmask == 0xff00 && bmask == 0xff && amask == 0xff000000: // Go ahead, it's a regular 32-bit image default: - return errBmpUnsupported + return newVipsError("unsupported BMP image") } default: - return errBmpUnsupported + return newVipsError("unsupported BMP image") } var palette []Color @@ -497,7 +492,7 @@ func (img *Image) loadBmp(data []byte, noAlpha bool) error { return img.decodeBmpRGB(r, width, height, 4, topDown, noAlpha) } - return errBmpUnsupported + return newVipsError("unsupported BMP image") } func (img *Image) saveAsBmp() (*imagedata.ImageData, error) { diff --git a/vips/color.go b/vips/color.go index fa47a7db..dee5d775 100644 --- a/vips/color.go +++ b/vips/color.go @@ -18,7 +18,7 @@ func ColorFromHex(hexcolor string) (Color, error) { c := Color{} if !hexColorRegex.MatchString(hexcolor) { - return c, fmt.Errorf("Invalid hex color: %s", hexcolor) + return c, newColorError("Invalid hex color: %s", hexcolor) } if len(hexcolor) == 3 { diff --git a/vips/errors.go b/vips/errors.go index 3e9a921d..58c65049 100644 --- a/vips/errors.go +++ b/vips/errors.go @@ -1,13 +1,32 @@ package vips import ( + "fmt" + "github.com/imgproxy/imgproxy/v3/ierrors" ) -type VipsError string +type ( + VipsError string + ColorError string +) func newVipsError(msg string) error { - return ierrors.Wrap(VipsError(msg), 2) + return ierrors.Wrap(VipsError(msg), 1) +} + +func newVipsErrorf(format string, args ...interface{}) error { + return ierrors.Wrap(VipsError(fmt.Sprintf(format, args...)), 1) } func (e VipsError) Error() string { return string(e) } + +func newColorError(format string, args ...interface{}) error { + return ierrors.Wrap( + ColorError(fmt.Sprintf(format, args...)), + 1, + ierrors.WithShouldReport(false), + ) +} + +func (e ColorError) Error() string { return string(e) } diff --git a/vips/ico.go b/vips/ico.go index 5ef822bc..2b7a9552 100644 --- a/vips/ico.go +++ b/vips/ico.go @@ -7,8 +7,6 @@ import "C" import ( "bytes" "encoding/binary" - "errors" - "fmt" "unsafe" "github.com/imgproxy/imgproxy/v3/imagedata" @@ -43,7 +41,7 @@ func (img *Image) loadIco(data []byte, shrink int, scale float64, pages int) err internalType = meta.Format() if internalType == imagetype.ICO || !SupportsLoad(internalType) { - return fmt.Errorf("Can't load %s from ICO", internalType) + return newVipsErrorf("Can't load %s from ICO", internalType) } imgdata := imagedata.ImageData{ @@ -56,7 +54,7 @@ func (img *Image) loadIco(data []byte, shrink int, scale float64, pages int) err func (img *Image) saveAsIco() (*imagedata.ImageData, error) { if img.Width() > 256 || img.Height() > 256 { - return nil, errors.New("Image dimensions is too big. Max dimension size for ICO is 256") + return nil, newVipsError("Image dimensions is too big. Max dimension size for ICO is 256") } var ptr unsafe.Pointer diff --git a/vips/vips.go b/vips/vips.go index da2dc2d3..cb9e69a4 100644 --- a/vips/vips.go +++ b/vips/vips.go @@ -9,7 +9,6 @@ package vips import "C" import ( "context" - "errors" "math" "net/http" "os" @@ -69,7 +68,7 @@ func Init() error { if err := C.vips_initialize(); err != 0 { C.vips_shutdown() - return errors.New("unable to start vips!") + return newVipsError("unable to start vips!") } // Disable libvips cache. Since processing pipeline is fine tuned, we won't get much profit from it. @@ -355,7 +354,7 @@ func (img *Image) Load(imgdata *imagedata.ImageData, shrink int, scale float64, case imagetype.TIFF: err = C.vips_tiffload_go(data, dataSize, &tmp) default: - return errors.New("Usupported image type to load") + return newVipsError("Usupported image type to load") } if err != 0 { return Error() @@ -376,7 +375,7 @@ func (img *Image) Load(imgdata *imagedata.ImageData, shrink int, scale float64, func (img *Image) LoadThumbnail(imgdata *imagedata.ImageData) error { if imgdata.Type != imagetype.HEIC && imgdata.Type != imagetype.AVIF { - return errors.New("Usupported image type to load thumbnail") + return newVipsError("Usupported image type to load thumbnail") } var tmp *C.VipsImage @@ -428,7 +427,7 @@ func (img *Image) Save(imgtype imagetype.Type, quality int) (*imagedata.ImageDat case imagetype.TIFF: err = C.vips_tiffsave_go(img.VipsImage, &ptr, &imgsize, C.int(quality)) default: - return nil, errors.New("Usupported image type to save") + return nil, newVipsError("Usupported image type to save") } if err != 0 { cancel()