Changeset 0.22.0 (#93)

This commit is contained in:
2026-03-01 23:57:33 +00:00
parent 3423738286
commit 12e03cec8b
5 changed files with 385 additions and 3 deletions

View File

@@ -320,7 +320,7 @@ func (h *ChannelHandler) CreateChannel(c *gin.Context) {
).Scan(
&ch.ID, &ch.UserID, &ch.Title, &ch.Type, &ch.Description, &ch.Model, &ch.APIConfigID,
&ch.SystemPrompt, &ch.IsArchived, &ch.IsPinned, &ch.Folder, &ch.ProjectID, &ch.WorkspaceID,
pq.Array(&tags), &ch.Settings, &ch.CreatedAt, &ch.UpdatedAt,
pq.Array(&tags), scanJSON(&ch.Settings), &ch.CreatedAt, &ch.UpdatedAt,
)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create channel"})
@@ -478,6 +478,11 @@ func (h *ChannelHandler) UpdateChannel(c *gin.Context) {
addClause("tags", writeTagsArg(req.Tags))
}
if req.Settings != nil {
// Validate settings is well-formed JSON before writing
if !json.Valid([]byte(*req.Settings)) {
c.JSON(http.StatusBadRequest, gin.H{"error": "settings must be valid JSON"})
return
}
// JSONB merge: new settings keys overwrite existing, unmentioned keys preserved
if database.IsSQLite() {
setClauses = append(setClauses, "settings = json_patch(settings, ?)")

View File

@@ -46,7 +46,15 @@ func (s *jsonScanner) Scan(src interface{}) error {
*s.dest = json.RawMessage("{}")
return nil
}
*s.dest = json.RawMessage(v)
// 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 in v0.21.7.
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) {

View File

@@ -183,3 +183,70 @@ func TestSafeJSON_PaginatedWithCorrupt(t *testing.T) {
t.Fatalf("expected 500 for paginated response with corrupt item, got %d", w.Code)
}
}
// ── Driver buffer aliasing tests ────────────
// These verify the fix for the v0.21.7 corruption bug: scanJSON must
// copy []byte from the database driver, not alias it. Without the copy,
// rows.Next() can overwrite the buffer and corrupt previously-scanned
// json.RawMessage values in a paginated list.
func TestScanJSON_ByteSliceNotAliased(t *testing.T) {
var dest json.RawMessage
s := scanJSON(&dest)
// Simulate driver buffer with valid JSON
buf := []byte(`{"key":"value"}`)
if err := s.Scan(buf); err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Simulate driver reusing the buffer on next rows.Scan()
copy(buf, []byte(`invalid_json!!`))
// dest must still hold the original valid value
if !json.Valid(dest) {
t.Fatalf("dest corrupted after source buffer reuse: %q", dest)
}
if string(dest) != `{"key":"value"}` {
t.Fatalf("expected original value, got %q", dest)
}
}
func TestScanJSON_ByteSliceIsolation_MultiRow(t *testing.T) {
// Simulate scanning two rows through the same driver buffer,
// as happens in ListChannels when lib/pq reuses memory.
var dest1, dest2 json.RawMessage
// Row 1
buf := make([]byte, 64)
row1 := []byte(`{"row":1,"selector":"abc"}`)
copy(buf, row1)
s1 := scanJSON(&dest1)
if err := s1.Scan(buf[:len(row1)]); err != nil {
t.Fatalf("row 1 scan error: %v", err)
}
// Row 2 — driver reuses same buffer
row2 := []byte(`{"row":2}`)
copy(buf, row2)
s2 := scanJSON(&dest2)
if err := s2.Scan(buf[:len(row2)]); err != nil {
t.Fatalf("row 2 scan error: %v", err)
}
// Both must retain their original values
if string(dest1) != `{"row":1,"selector":"abc"}` {
t.Fatalf("dest1 corrupted by row 2 scan: %q", dest1)
}
if string(dest2) != `{"row":2}` {
t.Fatalf("dest2 unexpected value: %q", dest2)
}
// Both must be independently valid JSON
if !json.Valid(dest1) {
t.Fatalf("dest1 not valid JSON after multi-row scan: %q", dest1)
}
if !json.Valid(dest2) {
t.Fatalf("dest2 not valid JSON after multi-row scan: %q", dest2)
}
}