check / check (push) Failing after 2s
auto is a format in the /v1/image/ path, an encrypted URL's token and the generator page. Once the signature or token is checked, pixa chooses AVIF when Accept names image/avif, else WebP when it names image/webp, else JPEG when the most specific of image/jpeg, image/* and */* allows it or Accept is absent. q=0 refuses a format; a header allowing none of the three answers 406, one that does not parse 400. The signature and token cover auto itself; the cache key and ETag use the chosen format. Answers from then on carry Vary: Accept. Model: opus-5-5
132 lines
3.4 KiB
Go
132 lines
3.4 KiB
Go
package handlers
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"mime"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"sneak.berlin/go/pixa/internal/imgcache"
|
|
)
|
|
|
|
// Errors for an Accept header that an auto URL cannot be served for.
|
|
var (
|
|
errInvalidAccept = errors.New("invalid Accept header")
|
|
errNotAcceptable = errors.New(
|
|
"not acceptable: auto serves image/avif, image/webp or image/jpeg")
|
|
)
|
|
|
|
// chooseAutoFormat replaces the format auto in req with the format
|
|
// formatForAccept chooses from r's Accept header, and adds Vary: Accept to the
|
|
// response, which then depends on that header. It answers 400 for an Accept
|
|
// header that is not valid and 406 for one that allows none of the formats,
|
|
// and reports whether req can be served. Any other format is left as it is.
|
|
func (s *Handlers) chooseAutoFormat(
|
|
w http.ResponseWriter, r *http.Request, req *imgcache.ImageRequest,
|
|
) bool {
|
|
if req.Format != imgcache.FormatAuto {
|
|
return true
|
|
}
|
|
|
|
w.Header().Add("Vary", "Accept")
|
|
|
|
format, err := formatForAccept(strings.Join(r.Header.Values("Accept"), ","))
|
|
if errors.Is(err, errNotAcceptable) {
|
|
s.respondError(w, err.Error(), http.StatusNotAcceptable)
|
|
|
|
return false
|
|
}
|
|
|
|
if err != nil {
|
|
s.respondError(w, err.Error(), http.StatusBadRequest)
|
|
|
|
return false
|
|
}
|
|
|
|
req.Format = format
|
|
|
|
return true
|
|
}
|
|
|
|
// formatForAccept returns the format an auto URL is served in for the Accept
|
|
// header accept: AVIF when it names image/avif, else WebP when it names
|
|
// image/webp, else JPEG when its most specific entry of image/jpeg, image/*
|
|
// and */* allows it, or when it names nothing. A q of 0 refuses a format.
|
|
// AVIF and WebP must be named, as clients that cannot show them send image/*
|
|
// and */* too.
|
|
func formatForAccept(accept string) (imgcache.ImageFormat, error) {
|
|
qualities, err := parseAccept(accept)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if len(qualities) == 0 {
|
|
return imgcache.FormatJPEG, nil
|
|
}
|
|
|
|
if qualities["image/avif"] > 0 {
|
|
return imgcache.FormatAVIF, nil
|
|
}
|
|
|
|
if qualities["image/webp"] > 0 {
|
|
return imgcache.FormatWebP, nil
|
|
}
|
|
|
|
// For JPEG, the most specific entry the header has decides
|
|
quality, named := qualities["image/jpeg"]
|
|
if !named {
|
|
quality, named = qualities["image/*"]
|
|
}
|
|
|
|
if !named {
|
|
quality = qualities["*/*"]
|
|
}
|
|
|
|
if quality > 0 {
|
|
return imgcache.FormatJPEG, nil
|
|
}
|
|
|
|
return "", errNotAcceptable
|
|
}
|
|
|
|
// parseAccept returns the q of each media range the Accept header accept
|
|
// names, 1 where it gives none. A media range named more than once keeps its
|
|
// lowest q, so a refusal is never overridden. A media range that does not
|
|
// parse, or a q that is not a number from 0 to 1, is an error.
|
|
func parseAccept(accept string) (map[string]float64, error) {
|
|
qualities := make(map[string]float64)
|
|
|
|
for entry := range strings.SplitSeq(accept, ",") {
|
|
// A header field list may hold empty entries
|
|
if strings.TrimSpace(entry) == "" {
|
|
continue
|
|
}
|
|
|
|
mediaRange, params, err := mime.ParseMediaType(entry)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: %q: %w", errInvalidAccept, entry, err)
|
|
}
|
|
|
|
quality := 1.0
|
|
|
|
if qParam, given := params["q"]; given {
|
|
quality, err = strconv.ParseFloat(qParam, 64)
|
|
inRange := quality >= 0 && quality <= 1
|
|
|
|
if err != nil || !inRange {
|
|
return nil, fmt.Errorf("%w: %q: q is not a number from 0 to 1",
|
|
errInvalidAccept, entry)
|
|
}
|
|
}
|
|
|
|
previous, named := qualities[mediaRange]
|
|
if !named || quality < previous {
|
|
qualities[mediaRange] = quality
|
|
}
|
|
}
|
|
|
|
return qualities, nil
|
|
}
|