fix: v0.2 review sweep

Controller-ruled small fixes accumulated from T3/T4 reviews:

1. routes_media.go: multipart Content-Type check is now an exact
   mediaType != "multipart/form-data" comparison (HasPrefix admitted
   "multipart/form-datax"); a malformed-tail multipart body (fails to
   parse at all) now reports "malformed multipart body", distinct from
   "model is required" (a body that parses fine but carries no model
   part) and "duplicate model field".
2. provider_openai.go: audioTranscription's doc comment no longer claims
   the body is always "replayed unchanged"/the boundary always
   "verbatim" — it now reflects the rewrite path routes_media.go
   already takes for an aliased model id.
3. rewriteMultipartModel: buf.Grow(len(body)+512) up front, avoiding
   repeated reallocate-and-copy growth while rebuilding an aliased
   multipart body.
4. limits.go currentUsage: guards len(vals) == len(keys) before indexing
   storeGetMulti's result, so a non-conforming counterStore's short slice
   reports storeDown instead of panicking.
5. admin.go dashboard JS: the 5s poll no longer wipes an existing
   auth-error message when no key is stored (only a successful load or a
   new submission clears it now); clearStoredKey on a 401/403 now only
   fires when the failing request's own key still matches what's
   currently stored, guarding against a late response for a key the user
   has since replaced.
6. Spec §4's duplicated /admin/api/overview bullet and the
   nosniff/no-store sentence's placement were already fixed upstream
   (docs/superpowers/specs/2026-08-20-llmgateway-v02-design.md, commit
   c02010d) — verified identical, nothing further to commit here.
7. Added TestAdmin_JSONSecurityHeaders (nosniff/no-store/CSP on both
   admin JSON APIs) and TestHandleAudioTranscriptions_MalformedTail_400
   (covers fix 1's new malformed-body branch).

go test ./... -count=1 -race, gofmt, vet, make yaegi-check, golangci-lint
(incl. gosec) all pass.
This commit is contained in:
2026-08-20 20:15:06 +01:00
parent 25578fb282
commit 63459b0c24
6 changed files with 149 additions and 14 deletions
+20 -3
View File
@@ -8,7 +8,6 @@ import (
"mime"
"mime/multipart"
"net/http"
"strings"
)
// Route paths for the three unified media endpoints (spec §3, v0.2):
@@ -182,7 +181,7 @@ func (g *Gateway) handleAudioTranscriptions(w http.ResponseWriter, r *http.Reque
writeOAIError(sw, http.StatusBadRequest, "invalid_request_error", "invalid Content-Type")
return
}
if !strings.HasPrefix(mediaType, "multipart/form-data") {
if mediaType != "multipart/form-data" {
writeOAIError(sw, http.StatusBadRequest, "invalid_request_error", "Content-Type must be multipart/form-data")
return
}
@@ -192,12 +191,23 @@ func (g *Gateway) handleAudioTranscriptions(w http.ResponseWriter, r *http.Reque
return
}
// The three failure shapes below are kept distinct rather than folded
// into one "model is required" 400 (review sweep, 2026-08-20): a
// duplicate "model" part is ambiguous, a body extractMultipartModel
// cannot even parse (a malformed or truncated tail) never reached a
// point where "model" could be evaluated at all, and only a
// successfully parsed body with no "model" part is actually missing
// one — each names its own actual problem instead of collapsing every
// case into the same misleading message.
requestedModel, err := extractMultipartModel(body, boundary)
switch {
case errors.Is(err, errDuplicateModelField):
writeOAIError(sw, http.StatusBadRequest, "invalid_request_error", "duplicate model field")
return
case err != nil || requestedModel == "":
case err != nil:
writeOAIError(sw, http.StatusBadRequest, "invalid_request_error", "malformed multipart body")
return
case requestedModel == "":
writeOAIError(sw, http.StatusBadRequest, "invalid_request_error", "model is required")
return
}
@@ -319,6 +329,13 @@ func extractMultipartModel(body []byte, boundary string) (string, error) {
func rewriteMultipartModel(body []byte, boundary, upstreamModel string) ([]byte, string, error) {
mr := multipart.NewReader(bytes.NewReader(body), boundary)
var buf bytes.Buffer
// The rebuilt body is at least as large as the original (same parts,
// same content, only the "model" value and the boundary differ) plus
// the fresh Writer's own per-part header/boundary overhead — growing
// buf to body's length up front, with headroom, avoids the repeated
// reallocate-and-copy growth bytes.Buffer would otherwise do one
// io.Copy at a time.
buf.Grow(len(body) + 512)
mw := multipart.NewWriter(&buf)
for {