You've already forked imgproxy
mirror of
https://github.com/imgproxy/imgproxy.git
synced 2026-06-11 22:04:13 +02:00
190 lines
4.7 KiB
Go
190 lines
4.7 KiB
Go
package fetcher
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
|
|
"github.com/imgproxy/imgproxy/v4/errctx"
|
|
"github.com/imgproxy/imgproxy/v4/fetcher/transport/generichttp"
|
|
)
|
|
|
|
const msgSourceIsUnreachable = "Source is unreachable"
|
|
|
|
type (
|
|
RequestError struct{ *errctx.WrappedError }
|
|
RequstSchemeError struct{ *errctx.TextError }
|
|
PartialResponseError struct{ *errctx.TextError }
|
|
ResponseStatusError struct{ *errctx.TextError }
|
|
TooManyRedirectsError struct{ *errctx.TextError }
|
|
RequestCanceledError struct{ *errctx.WrappedError }
|
|
RequestTimeoutError struct{ *errctx.WrappedError }
|
|
|
|
NotModifiedError struct {
|
|
*errctx.TextError
|
|
|
|
headers http.Header
|
|
}
|
|
)
|
|
|
|
func newRequestError(err error) error {
|
|
return RequestError{errctx.NewWrappedError(
|
|
err,
|
|
1,
|
|
errctx.WithStatusCode(http.StatusNotFound),
|
|
errctx.WithPublicMessage(msgSourceIsUnreachable),
|
|
errctx.WithShouldReport(false),
|
|
)}
|
|
}
|
|
|
|
var schemeEnvVars = map[string]string{
|
|
"local": "IMGPROXY_LOCAL_FILESYSTEM_ROOT",
|
|
"s3": "IMGPROXY_USE_S3",
|
|
"gs": "IMGPROXY_USE_GCS",
|
|
"abs": "IMGPROXY_USE_ABS",
|
|
"swift": "IMGPROXY_USE_SWIFT",
|
|
}
|
|
|
|
const schemeDocsUrl = "https://docs.imgproxy.net/configuration/options#"
|
|
|
|
func newRequestSchemeError(scheme string) error {
|
|
msg := fmt.Sprintf("unknown scheme: %s", scheme)
|
|
|
|
opts := []errctx.Option{
|
|
errctx.WithStatusCode(http.StatusNotFound),
|
|
errctx.WithPublicMessage(msgSourceIsUnreachable),
|
|
errctx.WithShouldReport(false),
|
|
}
|
|
|
|
if envVar, ok := schemeEnvVars[scheme]; ok {
|
|
msg = fmt.Sprintf("unknown scheme: %s (set %s to enable)", scheme, envVar)
|
|
opts = append(opts, errctx.WithDocsURL(schemeDocsUrl+envVar))
|
|
}
|
|
|
|
return RequstSchemeError{errctx.NewTextError(msg, 1, opts...)}
|
|
}
|
|
|
|
func newPartialResponseError(msg string) error {
|
|
return PartialResponseError{errctx.NewTextError(
|
|
msg,
|
|
1,
|
|
errctx.WithStatusCode(http.StatusNotFound),
|
|
errctx.WithPublicMessage(msgSourceIsUnreachable),
|
|
errctx.WithShouldReport(false),
|
|
)}
|
|
}
|
|
|
|
func newResponseStatusError(status int, body string) error {
|
|
var msg string
|
|
|
|
if len(body) > 0 {
|
|
msg = fmt.Sprintf("status: %d; %s", status, body)
|
|
} else {
|
|
msg = fmt.Sprintf("status: %d", status)
|
|
}
|
|
|
|
statusCode := 404
|
|
if status >= 400 && status < 500 {
|
|
statusCode = status
|
|
} else if status >= 500 {
|
|
statusCode = http.StatusBadGateway
|
|
}
|
|
|
|
return ResponseStatusError{errctx.NewTextError(
|
|
msg,
|
|
1,
|
|
errctx.WithStatusCode(statusCode),
|
|
errctx.WithPublicMessage(msgSourceIsUnreachable),
|
|
errctx.WithShouldReport(false),
|
|
)}
|
|
}
|
|
|
|
func newTooManyRedirectsError(n int) error {
|
|
return TooManyRedirectsError{errctx.NewTextError(
|
|
fmt.Sprintf("stopped after %d redirects", n),
|
|
1,
|
|
errctx.WithStatusCode(http.StatusNotFound),
|
|
errctx.WithPublicMessage(msgSourceIsUnreachable),
|
|
errctx.WithShouldReport(false),
|
|
)}
|
|
}
|
|
|
|
func newRequestCanceledError(err error) error {
|
|
return RequestCanceledError{errctx.NewWrappedError(
|
|
err,
|
|
2,
|
|
errctx.WithPrefix("source request is cancelled"),
|
|
errctx.WithStatusCode(499),
|
|
errctx.WithPublicMessage(msgSourceIsUnreachable),
|
|
errctx.WithShouldReport(false),
|
|
)}
|
|
}
|
|
|
|
func newRequestTimeoutError(err error) error {
|
|
return RequestTimeoutError{errctx.NewWrappedError(
|
|
err,
|
|
2,
|
|
errctx.WithPrefix("source request timed out"),
|
|
errctx.WithStatusCode(http.StatusGatewayTimeout),
|
|
errctx.WithPublicMessage(msgSourceIsUnreachable),
|
|
errctx.WithShouldReport(false),
|
|
)}
|
|
}
|
|
|
|
func newNotModifiedError(headers http.Header) error {
|
|
return NotModifiedError{
|
|
errctx.NewTextError(
|
|
"not modified",
|
|
1,
|
|
errctx.WithStatusCode(http.StatusNotModified),
|
|
errctx.WithPublicMessage("Not modified"),
|
|
errctx.WithShouldReport(false),
|
|
),
|
|
headers,
|
|
}
|
|
}
|
|
|
|
func (e NotModifiedError) Headers() http.Header {
|
|
return e.headers
|
|
}
|
|
|
|
// WrapError wraps generic error into fetcher-specific error types
|
|
func WrapError(err error, skipStack int) error {
|
|
type httpError interface {
|
|
Timeout() bool
|
|
}
|
|
|
|
var srcErr generichttp.SourceAddressError
|
|
|
|
switch {
|
|
case errors.Is(err, context.DeadlineExceeded):
|
|
return newRequestTimeoutError(err)
|
|
case errors.Is(err, context.Canceled):
|
|
return newRequestCanceledError(err)
|
|
case errors.Is(err, io.ErrUnexpectedEOF):
|
|
return PartialResponseError{errctx.NewTextError(
|
|
"response is incomplete",
|
|
1,
|
|
errctx.WithStatusCode(http.StatusUnprocessableEntity),
|
|
errctx.WithPublicMessage("Source response is incomplete"),
|
|
errctx.WithShouldReport(false),
|
|
)}
|
|
case errors.As(err, &srcErr):
|
|
return srcErr
|
|
default:
|
|
if httpErr, ok := err.(httpError); ok && httpErr.Timeout() {
|
|
return newRequestTimeoutError(err)
|
|
}
|
|
|
|
//nolint:errorlint // Check for *url.Error itself, not wrapped error
|
|
if _, ok := err.(*url.Error); ok {
|
|
return newRequestError(err)
|
|
}
|
|
}
|
|
|
|
return errctx.WrapWithStackSkip(err, skipStack+1)
|
|
}
|