diff --git a/README.md b/README.md index e8e8df4..8200596 100644 --- a/README.md +++ b/README.md @@ -190,9 +190,11 @@ path under `/v1/` answers 200, in maintenance mode too. the cache and the upstream fetch; 403 when the upstream host, or a host it redirects to, is `localhost`, ends in `.localhost` or `.local`, or has an address in a blocked network (see `blocked_networks`); 429 over - `image_requests_per_minute` (below); 502 when the upstream answered with an - error status, and for 5 minutes after that for the same source URL; 503 when - pixa is busy or in maintenance mode; 500 for any other failure. + `image_requests_per_minute` (below); 415 when the upstream answers with a + `Content-Type` that is not one of the source formats (below), or for `orig` of + a source pixa cannot save in its own format; 502 when the upstream answered + with an error status, and for 5 minutes after that for the same source URL; + 503 when pixa is busy or in maintenance mode; 500 for any other failure. - `GET` or `HEAD` `/v1/e//` — an image through an encrypted URL (see Encrypted URLs). Needs: nothing but the URL. Answers: 200; 304 when `If-None-Match` matches the image's `ETag`; 400 for a token that does not @@ -200,7 +202,7 @@ path under `/v1/` answers 200, in maintenance mode too. expired; 504 when the upstream has not sent its response headers within `upstream_fetch_timeout`, but 500 when that time runs out while the image itself is still arriving; 400 for an `Accept` header that is not valid, and - 406, 403, 429, 502, 503 and 500, as for `/v1/image/`. + 406, 403, 415, 429, 502, 503 and 500, as for `/v1/image/`. - `GET /robots.txt` — asks every crawler to stay away (`Disallow: /`). Needs: nothing. Answers: 200. - `GET /.well-known/healthcheck.json` — JSON with `status` (`ok`), `now`, @@ -260,9 +262,10 @@ once, is refused with 400. both optional (values under Signature Specification). Both are part of what is cached, so each value of either is a separate cached image. -The source image may be JPEG, PNG, GIF, WebP, AVIF, JPEG XL or SVG. The -upstream's `Content-Type` must name its format, and the image's first bytes must -match it. +The source image may be JPEG, PNG, GIF, WebP, AVIF or JPEG XL. The upstream's +`Content-Type` must name its format, and the image's first bytes must match it. +An upstream answering with any other `Content-Type`, SVG included, is refused +with 415. With the format `auto`, pixa chooses the format for each request from its `Accept` header, in this order: diff --git a/internal/handlers/image.go b/internal/handlers/image.go index 848a252..9978565 100644 --- a/internal/handlers/image.go +++ b/internal/handlers/image.go @@ -247,6 +247,14 @@ func (s *Handlers) respondImageError( return } + if errors.Is(err, httpfetcher.ErrInvalidContentType) || + errors.Is(err, imageprocessor.ErrUnsupportedInputFormat) { + s.respondError(w, "unsupported source format", + http.StatusUnsupportedMediaType) + + return + } + s.respondError(w, "internal error", http.StatusInternalServerError) } diff --git a/internal/handlers/imageenc.go b/internal/handlers/imageenc.go index bf79054..816e83f 100644 --- a/internal/handlers/imageenc.go +++ b/internal/handlers/imageenc.go @@ -161,6 +161,10 @@ func (s *Handlers) handleImageError(w http.ResponseWriter, err error) { errors.Is(err, imageprocessor.ErrTooManyImages): s.respondError(w, "server busy, try again later", http.StatusServiceUnavailable) + case errors.Is(err, httpfetcher.ErrInvalidContentType), + errors.Is(err, imageprocessor.ErrUnsupportedInputFormat): + s.respondError(w, "unsupported source format", + http.StatusUnsupportedMediaType) default: s.log.Error("image request failed", "error", err) s.respondError(w, "internal error", http.StatusInternalServerError) diff --git a/internal/httpfetcher/httpfetcher.go b/internal/httpfetcher/httpfetcher.go index dd37489..9d09295 100644 --- a/internal/httpfetcher/httpfetcher.go +++ b/internal/httpfetcher/httpfetcher.go @@ -158,7 +158,6 @@ func DefaultConfig() *Config { contentTypeWebP, contentTypeAVIF, contentTypeJXL, - contentTypeSVG, }, AllowHTTP: false, MaxConnectionsPerHost: DefaultMaxConnectionsPerHost, diff --git a/internal/imageprocessor/imageprocessor.go b/internal/imageprocessor/imageprocessor.go index 6d59b34..af58690 100644 --- a/internal/imageprocessor/imageprocessor.go +++ b/internal/imageprocessor/imageprocessor.go @@ -137,6 +137,11 @@ var ErrInputDataTooLarge = errors.New("input data exceeds maximum allowed size") // not supported. var ErrUnsupportedOutputFormat = errors.New("unsupported output format") +// ErrUnsupportedInputFormat is returned when the requested format is orig and +// the input is in a format the processor cannot name, so it cannot be saved +// in that format. +var ErrUnsupportedInputFormat = errors.New("unsupported input format") + // ErrTooManyImages is returned when MaxConcurrentProcessing images are being // processed and none finishes within ProcessingWaitTimeout. var ErrTooManyImages = errors.New("too many images being processed at once") @@ -256,10 +261,14 @@ func (p *ImageProcessor) Process( } } - // orig is the source's own format; encode refuses an empty format + // orig is the source's own format, and an error when the processor + // cannot name it; encode refuses an empty format outputFormat := req.Format if outputFormat == FormatOriginal { - outputFormat = p.formatFromString(inputFormat) + outputFormat, err = p.formatFromString(inputFormat) + if err != nil { + return nil, err + } } // Encode to target format @@ -687,22 +696,23 @@ func jxlDistance(quality int) float64 { return 53.0/3000.0*q*q - 23.0/20.0*q + 25.0 } -// formatFromString converts a format string to Format. -func (p *ImageProcessor) formatFromString(format string) Format { +// formatFromString converts a format string from detectFormat to Format. A +// format detectFormat could not name is ErrUnsupportedInputFormat. +func (p *ImageProcessor) formatFromString(format string) (Format, error) { switch format { case "jpeg": - return FormatJPEG + return FormatJPEG, nil case "png": - return FormatPNG + return FormatPNG, nil case "gif": - return FormatGIF + return FormatGIF, nil case "webp": - return FormatWebP + return FormatWebP, nil case string(FormatAVIF): - return FormatAVIF + return FormatAVIF, nil case string(FormatJXL): - return FormatJXL + return FormatJXL, nil default: - return FormatJPEG + return "", fmt.Errorf("%w: %s", ErrUnsupportedInputFormat, format) } }