Files
petal/internal/httputil/json.go
prosolis 8c6bc1604b Code-review follow-ups: httputil, validation caps, a11y
Backend:
- Extract shared internal/httputil (WriteJSON/ErrorJSON/BadRequest/
  ServerError); drop the triple-duplicated helpers in docs, suggestions,
  vocab. ServerError now logs the real error and returns a generic 500 so
  raw DB/internal errors never reach the client.
- vocab capture: validate doc_id ownership (blank -> none, unknown -> 400
  instead of a leaked FK 500); rune-safe clamp word/gloss/definition/
  phonetic/example.
- vocab review(): wrap the read-modify-write in a transaction (TOCTOU).
- /api request-size cap via MaxBytesReader middleware (2 MiB), exempting
  /api/images (own 10 MiB limit).

Frontend:
- StatusBar: drive the checking/voicing/collocating indicators from one
  array; llmDown uses !anyBusy.
- Slide-overs: new useFocusTrap hook (focus-in, Tab trap, focus-restore)
  on GardenPanel + HistoryPanel, both role=dialog/aria-modal/aria-label.
- speech.ts: export stopSpeech(); GardenPanel cancels audio on unmount.

Tests: add doc_id-validation and field-clamp coverage; full suite green.

Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
2026-06-26 16:59:23 -07:00

38 lines
1.4 KiB
Go

// Package httputil holds the small HTTP response helpers shared by every API
// handler package (docs, suggestions, vocab, …). Keeping them in one place means
// JSON encoding and — crucially — error handling behave identically everywhere:
// internal errors are logged in full but never leaked to the client.
package httputil
import (
"encoding/json"
"log"
"net/http"
)
// WriteJSON encodes v as the response body with the given status code.
func WriteJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(v)
}
// ErrorJSON sends a {"error": msg} body with the given status. The message is
// caller-chosen and safe to show the client.
func ErrorJSON(w http.ResponseWriter, status int, msg string) {
WriteJSON(w, status, map[string]string{"error": msg})
}
// BadRequest is the common 400 shorthand.
func BadRequest(w http.ResponseWriter, msg string) {
ErrorJSON(w, http.StatusBadRequest, msg)
}
// ServerError logs the real error (with full detail, for the operator) and
// returns a generic 500 to the client — raw database/internal errors must never
// reach the browser, where they leak schema and implementation details.
func ServerError(w http.ResponseWriter, err error) {
log.Printf("internal error: %v", err)
ErrorJSON(w, http.StatusInternalServerError, "something went wrong")
}