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
38 lines
1.4 KiB
Go
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")
|
|
}
|