package handlers import ( "encoding/json" "log" "net/http" "github.com/gin-gonic/gin" "github.com/lib/pq" "armature/database" ) // ── SafeJSON ──────────────────────────────── // SafeJSON pre-marshals the response body, guaranteeing that a // serialisation failure produces a clean 500 instead of a truncated // 200 with half-written JSON. Use for any endpoint whose response // contains json.RawMessage or other pass-through data from the DB. func SafeJSON(c *gin.Context, code int, obj interface{}) { body, err := json.Marshal(obj) if err != nil { log.Printf("⚠ SafeJSON: marshal failed: %v", err) c.JSON(http.StatusInternalServerError, gin.H{ "error": "response serialization failed", }) return } c.Data(code, "application/json; charset=utf-8", body) } // ── JSON column scanner ───────────────────── // Handles both Postgres ([]byte) and SQLite (string) return types. // Validates content before accepting it — corrupt data is replaced // with "{}" and a warning is logged so admins can find the bad row. type jsonScanner struct{ dest *json.RawMessage } func scanJSON(dest *json.RawMessage) *jsonScanner { return &jsonScanner{dest: dest} } func (s *jsonScanner) Scan(src interface{}) error { switch v := src.(type) { case []byte: if len(v) == 0 || !json.Valid(v) { log.Printf("⚠ scanJSON: invalid JSON in column (len=%d, preview=%.60q), defaulting to {}", len(v), v) *s.dest = json.RawMessage("{}") return nil } // CRITICAL: copy the driver buffer. json.RawMessage(v) aliases the // underlying []byte owned by database/sql. The driver may reuse that // memory on the next rows.Scan() call, corrupting our data after the // fact. This caused intermittent SafeJSON 500s on paginated list // endpoints where row N's Settings pointed to row N+1's overwritten // buffer. Fixed. cp := make([]byte, len(v)) copy(cp, v) *s.dest = json.RawMessage(cp) case string: b := []byte(v) if len(b) == 0 || !json.Valid(b) { log.Printf("⚠ scanJSON: invalid JSON in column (len=%d, preview=%.60q), defaulting to {}", len(b), b) *s.dest = json.RawMessage("{}") return nil } *s.dest = json.RawMessage(b) case nil: *s.dest = json.RawMessage("{}") default: *s.dest = json.RawMessage("{}") } return nil } // ── Tags column scanner ───────────────────── // SQLite stores tags as a JSON text array; Postgres uses text[]. // Both paths validate and fall back to an empty slice on error. type tagsScanner struct { dest *[]string } func scanTags(dest *[]string) *tagsScanner { return &tagsScanner{dest: dest} } func (s *tagsScanner) Scan(src interface{}) error { if src == nil { *s.dest = []string{} return nil } if database.IsSQLite() { var raw string switch v := src.(type) { case string: raw = v case []byte: raw = string(v) default: *s.dest = []string{} return nil } if raw == "" { *s.dest = []string{} return nil } var arr []string if err := json.Unmarshal([]byte(raw), &arr); err != nil { log.Printf("⚠ scanTags: invalid JSON array in column (preview=%.60q), defaulting to []", raw) *s.dest = []string{} return nil } *s.dest = arr return nil } // Postgres: delegate to pq, but catch errors if err := pq.Array(s.dest).Scan(src); err != nil { log.Printf("⚠ scanTags: pq.Array scan failed (%v), defaulting to []", err) *s.dest = []string{} return nil } return nil }