fix(landscape): accept an empty run body; pin the builder's Alpine release

POST /landscape/runs documents an optional body, but DecodeJSON returned
io.EOF on an empty one and the handler turned that into 400 — breaking the
most common call, an all-defaults run. decodeRunRequest now treats io.EOF as
the zero request; a malformed body is still 400.

The Dockerfile's Alpine ABI coupling was enforced only by a comment: a
floating golang:1.26-alpine builder against a pinned alpine:3.24 runtime
would build green and die at startup on a libgdal soname mismatch as soon as
the Go image rolls forward. Both stages are now pinned to 3.24.

Also: the route-mount test walks the router so all six routes are asserted
(GET /slots and GET /results included), and the spec documents the 404 those
two return when migration 00011's seeded groups are absent.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-09-05 14:22:26 +05:00
parent ad3c9fbdcd
commit 6aec00ee29
4 changed files with 129 additions and 26 deletions

View File

@@ -1,6 +1,8 @@
package http
import (
"errors"
"io"
"net/http"
"gis/internal/landscape"
@@ -89,12 +91,26 @@ func (h *LandscapeHandler) uploadSlot(w http.ResponseWriter, r *http.Request) {
httputil.WriteJSON(w, http.StatusCreated, dataset)
}
// decodeRunRequest reads the run body. The whole body is optional — every field
// has a default — so an empty body is the zero request (a default incremental
// run), not an error. A malformed body writes a 400 and reports false.
func decodeRunRequest(w http.ResponseWriter, r *http.Request) (runRequest, bool) {
req, err := httputil.DecodeJSON[runRequest](w, r)
switch {
case errors.Is(err, io.EOF):
return runRequest{}, true
case err != nil:
httputil.WriteError(w, http.StatusBadRequest, "invalid request body")
return runRequest{}, false
}
return req, true
}
// createRun queues a Landscape Program run and returns it with 202 Accepted;
// the worker executes it asynchronously.
func (h *LandscapeHandler) createRun(w http.ResponseWriter, r *http.Request) {
req, err := httputil.DecodeJSON[runRequest](w, r)
if err != nil {
httputil.WriteError(w, http.StatusBadRequest, "invalid request body")
req, ok := decodeRunRequest(w, r)
if !ok {
return
}
if err := h.validate.Struct(req); err != nil {