2 Commits
Author SHA1 Message Date
sneak 98d23ad32e Refuse a q outside 1-100 on /v1/image/ with 400 (closes #134)
check / check (push) Successful in 3m16s
The route ignored a q that was not a number or was outside 1-100 and
used 85, so q=banana or q=500 was served as if q were absent and
verified against a signature made for 85.

It now reads q with the check the URL generator uses for its quality
field (parseFormInt with minQuality and maxQuality, default
encurl.DefaultQuality) and answers anything else with a 400 naming q
and the value. That check takes an empty value as missing, so an empty
q in the URL is refused before it. Only a q missing from the URL is
85. README.md states the range.

Model: opus-5-5
2026-09-28 14:44:25 +00:00
sneak a3dd1aad4e test: /v1/image/ answers an invalid q with 400 (closes #134)
Route tests for q=banana, q=0, q=101, an empty q= and q=&q=500: each
must be a 400 whose error names q and the value. They fail on next,
where each is served at the default quality 85.

Model: opus-5-5
2026-09-28 14:44:25 +00:00
4 changed files with 32 additions and 20 deletions
+5 -5
View File
@@ -31,11 +31,11 @@ exhaustion
# Completed Steps # Completed Steps
- 2026-09-28 refuse an invalid `q` on `/v1/image/` (closes #134): a `q` - 2026-09-28 refuse an invalid `q` on `/v1/image/` (closes #134): a `q`
that is not a whole number from 1 to 100 is a 400 naming `q` and the that is not a whole number from 1 to 100, an empty `q` included, is a
value, instead of being served at the default 85; the route reads `q` 400 naming `q` and the value, instead of being served at the default
with the generator's quality check (`parseFormInt` with `minQuality` 85; the route reads `q` with the generator's quality check
and `maxQuality`); an absent or empty `q` is still 85; `README.md` (`parseFormInt` with `minQuality` and `maxQuality`); only a `q` missing
states the range. from the URL is still 85; `README.md` states the range.
- 2026-09-28 unknown `PIXA_` environment variables abort startup (closes - 2026-09-28 unknown `PIXA_` environment variables abort startup (closes
#133): a variable whose name starts with `PIXA_` but is neither a #133): a variable whose name starts with `PIXA_` but is neither a
setting's variable nor `PIXA_CONFIG_PATH` aborts startup naming it, as setting's variable nor `PIXA_CONFIG_PATH` aborts startup naming it, as
+3 -3
View File
@@ -18,9 +18,9 @@ import (
"sneak.berlin/go/pixa/internal/templates" "sneak.berlin/go/pixa/internal/templates"
) )
// errInvalidFormField reports a generator form field whose value is // errInvalidFormField reports a generator form field, or the q parameter of
// non-numeric or out of range. The offending field name is wrapped in so the // /v1/image/, whose value is non-numeric or out of range. The offending field
// response can name it. // name is wrapped in so the response can name it.
var errInvalidFormField = errors.New("invalid") var errInvalidFormField = errors.New("invalid")
// Bounds for the generator's quality and ttl fields; the quality bounds also // Bounds for the generator's quality and ttl fields; the quality bounds also
+11 -8
View File
@@ -294,21 +294,24 @@ func TestHandleImage_InvalidFitMode_Returns400(t *testing.T) {
} }
// TestHandleImage_InvalidQuality_Returns400 verifies that the plain image // TestHandleImage_InvalidQuality_Returns400 verifies that the plain image
// route answers a q that is not a whole number from 1 to 100 with 400 naming // route answers a q that is not a whole number from 1 to 100, an empty one
// q and the value, instead of serving the image at the default quality 85. // included, with 400 naming q and the value, instead of serving the image at
// the default quality 85.
func TestHandleImage_InvalidQuality_Returns400(t *testing.T) { func TestHandleImage_InvalidQuality_Returns400(t *testing.T) {
t.Parallel() t.Parallel()
tests := []struct { tests := []struct {
q, wantError string query, wantError string
}{ }{
{"banana", `invalid q: not a number, got "banana"`}, {"q=banana", `invalid q: not a number, got "banana"`},
{"0", `invalid q: must be from 1 to 100, got "0"`}, {"q=0", `invalid q: must be from 1 to 100, got "0"`},
{"101", `invalid q: must be from 1 to 100, got "101"`}, {"q=101", `invalid q: must be from 1 to 100, got "101"`},
{"q=", `invalid q: not a number, got ""`},
{"q=&q=500", `invalid q: not a number, got ""`},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run("q="+tt.q, func(t *testing.T) { t.Run(tt.query, func(t *testing.T) {
t.Parallel() t.Parallel()
fix := setupTestHandler(t) fix := setupTestHandler(t)
@@ -317,7 +320,7 @@ func TestHandleImage_InvalidQuality_Returns400(t *testing.T) {
r.Get("/v1/image/*", fix.handler.HandleImage()) r.Get("/v1/image/*", fix.handler.HandleImage())
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, req := httptest.NewRequestWithContext(t.Context(), http.MethodGet,
"/v1/image/"+fix.goodHost+"/images/photo.jpg/50x50.jpeg?q="+tt.q, nil) "/v1/image/"+fix.goodHost+"/images/photo.jpg/50x50.jpeg?"+tt.query, nil)
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
r.ServeHTTP(rec, req) r.ServeHTTP(rec, req)
+13 -4
View File
@@ -102,13 +102,22 @@ func (s *Handlers) parseImageRequest(
} }
} }
// Parse optional quality and fit params. An absent q is 85; a q that is // Parse optional quality and fit params. Only a q missing from the URL is
// not a whole number from 1 to 100 is refused, checked as the generator // 85. A q in the URL that is not a whole number from 1 to 100, an empty
// checks its quality field. // one included, is refused, checked as the generator checks its quality
// field; that check alone would take an empty q as missing.
qStr := query.Get("q")
if query.Has("q") && qStr == "" {
s.respondError(w, `invalid q: not a number, got ""`,
http.StatusBadRequest)
return nil, false
}
req.Quality, err = parseFormInt(query, "q", req.Quality, err = parseFormInt(query, "q",
encurl.DefaultQuality, minQuality, maxQuality) encurl.DefaultQuality, minQuality, maxQuality)
if err != nil { if err != nil {
s.respondError(w, fmt.Sprintf("%v, got %q", err, query.Get("q")), s.respondError(w, fmt.Sprintf("%v, got %q", err, qStr),
http.StatusBadRequest) http.StatusBadRequest)
return nil, false return nil, false