Changeset 0.22.0 (#93)
This commit is contained in:
@@ -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, ?)")
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user