From e9a700b9d99681114841ca6ba4a1d789709927d9 Mon Sep 17 00:00:00 2001 From: logan Date: Sun, 31 May 2026 00:34:38 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20Redis=20=E6=9F=A5=E8=AF=A2=E6=A0=BC?= =?UTF-8?q?=E5=BC=8F=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - rows nil → 空数组: rowsOrDefault() 确保 HGETALL/LRANGE 空集合返回 [] 而非 null - 扩展只读白名单: PING/CONFIG/CLIENT/SLOWLOG/CLUSTER/MEMORY/OBJECT/DEBUG 等 - 前端 sortedResult: Array.isArray() 检查防止 raw_text 模式报 not iterable --- backend/internal/handler/instance_detail.go | 17 +++++++++++++++-- frontend/src/pages/QueriesPage.tsx | 1 + 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/backend/internal/handler/instance_detail.go b/backend/internal/handler/instance_detail.go index ea42bcf..ac757c1 100644 --- a/backend/internal/handler/instance_detail.go +++ b/backend/internal/handler/instance_detail.go @@ -705,6 +705,14 @@ func parseCommandParts(input string) []string { return parts } +// rowsOrDefault ensures rows is never nil (JSON null), always an empty slice +func rowsOrDefault(rows []interface{}) []interface{} { + if rows == nil { + return []interface{}{} + } + return rows +} + func executeRedisQuery(instance *model.Instance, dbSelect string, command string, limit int, offset int) (gin.H, error) { // Parse Redis DB number from dbSelect (e.g. "0", "1") redisDB := 0 @@ -731,12 +739,17 @@ func executeRedisQuery(instance *model.Instance, dbSelect string, command string readOnly := map[string]bool{ "GET": true, "MGET": true, "HGET": true, "HGETALL": true, "LRANGE": true, "SMEMBERS": true, "ZRANGE": true, - "TYPE": true, "TTL": true, "EXISTS": true, "KEYS": true, + "TYPE": true, "TTL": true, "PTTL": true, "EXISTS": true, "KEYS": true, "INFO": true, "DBSIZE": true, "SCAN": true, "STRLEN": true, "LLEN": true, "SCARD": true, "ZCARD": true, "HLEN": true, "HKEYS": true, "HVALS": true, "HSCAN": true, "SSCAN": true, "ZSCAN": true, "ZRANGEBYSCORE": true, "GETSET": true, "GETRANGE": true, "SUBSTR": true, + "PING": true, "CONFIG": true, "CLIENT": true, "SLOWLOG": true, + "CLUSTER": true, "MEMORY": true, "OBJECT": true, "DEBUG": true, + "SRANDMEMBER": true, "RANDOMKEY": true, "LASTSAVE": true, + "ROLE": true, "REPLCONF": true, "SENTINEL": true, + "LATENCY": true, "MODULE": true, "COMMAND": true, } if !readOnly[cmd] { return nil, fmt.Errorf("only read-only commands allowed, got: %s", cmd) @@ -949,7 +962,7 @@ func executeRedisQuery(instance *model.Instance, dbSelect string, command string return gin.H{ "columns": columns, - "rows": rows, + "rows": rowsOrDefault(rows), "total": len(rows), "count": len(rows), }, nil diff --git a/frontend/src/pages/QueriesPage.tsx b/frontend/src/pages/QueriesPage.tsx index 0d9dd95..c2e747e 100644 --- a/frontend/src/pages/QueriesPage.tsx +++ b/frontend/src/pages/QueriesPage.tsx @@ -244,6 +244,7 @@ export default function QueriesPage() { /* ---- Result Sorting ---- */ const sortedResult = useMemo(() => { if (!result || !sortState.column || !sortState.direction) return result; + if (!Array.isArray(result.rows)) return result; const col = sortState.column; const dir = sortState.direction === 'asc' ? 1 : -1; return { ...result, rows: [...result.rows].sort((a, b) => { const av = a[col] ?? ''; const bv = b[col] ?? '';