From c323fa3b4c37968b2fc2182f5f2576b0a7921fc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20=C4=B0hsan=20G=C3=B6rgel?= Date: Mon, 20 Jul 2026 17:18:39 +0300 Subject: [PATCH] fix: return an error instead of panicking when scanning JSON into an unaddressable value A non-nil interface field routes the scan to the element value, which is not addressable, so scanJSON panicked on dest.Addr() and the connection was left in idle-in-transaction. The JSON scan paths now pre-check CanAddr and fail with an error, as suggested in #1306. --- schema/scan.go | 9 +++++++++ schema/scan_json_test.go | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 schema/scan_json_test.go diff --git a/schema/scan.go b/schema/scan.go index 306f55f6f..3c6341d65 100644 --- a/schema/scan.go +++ b/schema/scan.go @@ -352,6 +352,9 @@ func scanJSON(dest reflect.Value, src any) error { if src == nil { return scanNull(dest) } + if !dest.CanAddr() { + return fmt.Errorf("bun: Scan(nonaddressable %s)", dest.Type()) + } b, err := toBytes(src) if err != nil { @@ -365,6 +368,9 @@ func scanJSONUseNumber(dest reflect.Value, src any) error { if src == nil { return scanNull(dest) } + if !dest.CanAddr() { + return fmt.Errorf("bun: Scan(nonaddressable %s)", dest.Type()) + } b, err := toBytes(src) if err != nil { @@ -531,6 +537,9 @@ func scanJSONIntoInterface(dest reflect.Value, src any) error { } dest = dest.Elem() + if !dest.CanAddr() { + return fmt.Errorf("bun: Scan(nonaddressable %s)", dest.Type()) + } if fn := Scanner(dest.Type()); fn != nil { return fn(dest, src) } diff --git a/schema/scan_json_test.go b/schema/scan_json_test.go new file mode 100644 index 000000000..e579251ee --- /dev/null +++ b/schema/scan_json_test.go @@ -0,0 +1,36 @@ +package schema + +import ( + "reflect" + "testing" + + "github.com/stretchr/testify/require" +) + +// Regression test for #1306: scanning JSON into a non-nil interface used to +// panic on dest.Addr() because the interface element is not addressable. The +// scan must fail with an error instead, so the query does not deadlock. +func TestScanJSONUnaddressableInterface(t *testing.T) { + var value any = map[string]any{"a": float64(1)} + dest := reflect.ValueOf(&value).Elem() + + fn := Scanner(dest.Type()) + require.NotNil(t, fn) + + require.NotPanics(t, func() { + err := fn(dest, []byte(`{"b":2}`)) + require.Error(t, err) + require.Contains(t, err.Error(), "nonaddressable") + }) +} + +func TestScanJSONAddressableMap(t *testing.T) { + var value map[string]any + dest := reflect.ValueOf(&value).Elem() + + fn := Scanner(dest.Type()) + require.NotNil(t, fn) + + require.NoError(t, fn(dest, []byte(`{"b":2}`))) + require.Equal(t, map[string]any{"b": float64(2)}, value) +}