diff --git a/backend/internal/handler/redis.go b/backend/internal/handler/redis.go index f931b5e..f4b806f 100644 --- a/backend/internal/handler/redis.go +++ b/backend/internal/handler/redis.go @@ -351,6 +351,18 @@ func writeRedisKeyValue(ctx context.Context, client *redisClientWrapper, key str } func getRedisClient(instance *model.Instance, db int) (*redisClientWrapper, error) { + // 防御:非 Redis 实例不能走 Redis 协议。 + // 若前端误调 /api/v1/redis/:id/*(如共用组件的 MySQL 实例), + // 用 Redis 协议连 3306 会收到 MySQL 握手包,报 + // `redis: invalid reply: "J\x00\x00\x00\n"` 并抛 500。 + // 这里提前拦截,给出可读错误。 + if instance.DBType != "redis" { + return nil, resp.UserFacing(fmt.Sprintf( + "实例「%s」的类型是 %s,不是 redis。Redis 接口只适用于 redis 实例;"+ + "MySQL/PostgreSQL 请使用 /api/v1/instances/%d/databases 等对应接口", + instance.Name, instance.DBType, instance.ID)) + } + password := instance.Password // Decrypt if encrypted if strings.HasPrefix(password, "enc:") { diff --git a/backend/internal/handler/resp/resp.go b/backend/internal/handler/resp/resp.go index 099371c..5a384fa 100644 --- a/backend/internal/handler/resp/resp.go +++ b/backend/internal/handler/resp/resp.go @@ -7,6 +7,7 @@ package resp import ( + "errors" "log" "net/http" @@ -54,7 +55,25 @@ func Forbidden(c *gin.Context, detail string) { Fail(c, http.StatusForbidden, func NotFound(c *gin.Context, detail string) { Fail(c, http.StatusNotFound, detail) } // InternalErr 500:服务端 log 完整错误,客户端只收通用文案(防 DB/内部错误原文泄漏) +// UserFacingError 标记「可以展示给用户」的错误。 +// +// 典型场景:调用方用错了接口/参数不满足前置条件(例如拿 MySQL 实例去调 +// /api/v1/redis/:id/*),这既不是服务端故障也不该泄漏内部细节, +// 用 400 + 明确原因比 500 + "服务器内部错误" 更有助于定位。 +type UserFacingError struct{ Msg string } + +func (e *UserFacingError) Error() string { return e.Msg } + +// UserFacing 构造一个可展示给用户的错误 +func UserFacing(msg string) error { return &UserFacingError{Msg: msg} } + func InternalErr(c *gin.Context, err error) { + var uf *UserFacingError + if errors.As(err, &uf) { + log.Printf("[resp] bad request on %s %s: %v", c.Request.Method, c.Request.URL.Path, err) + c.JSON(http.StatusBadRequest, gin.H{"detail": uf.Msg, "code": "bad_request"}) + return + } log.Printf("[resp] internal error on %s %s: %v", c.Request.Method, c.Request.URL.Path, err) c.JSON(http.StatusInternalServerError, gin.H{"detail": "服务器内部错误,请稍后重试", "code": "internal"}) } diff --git a/frontend/src/components/SchemaSidebar.tsx b/frontend/src/components/SchemaSidebar.tsx index 0299dfd..3d1f9d0 100644 --- a/frontend/src/components/SchemaSidebar.tsx +++ b/frontend/src/components/SchemaSidebar.tsx @@ -93,7 +93,8 @@ export default function SchemaSidebar({ // -- Redis State(浏览逻辑在共享 RedisKeyBrowser,这里只留库选择与详情预览) -- const [redisDb, setRedisDb] = useState(0); - const redisDbs = useRedisDbs(Number(instanceId)); + // 仅 Redis 实例才拉库列表,避免 MySQL/Mongo 也打到 /redis/:id/dbs + const redisDbs = useRedisDbs(Number(instanceId), isRedis); const ctxMenu = useContextMenu(); const dialect: SqlDialect = dbType === 'postgresql' ? 'postgresql' : 'mysql'; @@ -407,22 +408,32 @@ export default function SchemaSidebar({