1
0
mirror of https://github.com/redis/go-redis.git synced 2026-10-22 19:49:00 +03:00

fix(cmd): accept RESP3 doubles in Cmd.Float64 (#4088)

* fix(cmd): accept RESP3 doubles in Cmd.Float64

With RESP3, the default protocol, ReadReply returns a double reply
(for example from ZSCORE) as float64. toFloat64 and toFloat32 only
handled int64 and string, so Cmd.Float64, Float32, Float64Slice and
Float32Slice failed with "unexpected type=float64". Handle float64 the
same way toBool handles the RESP3 boolean.

* fix(cmd): keep Float32 range error on RESP3

float32(val) turns a RESP3 double outside the float32 range into an
infinity without an error, while the RESP2 bulk string path returns the
ParseFloat range error. Return the same error for the RESP3 double. An
infinity sent by the server is still returned as is.

---------

Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com>
This commit is contained in:
Yingzi Zhang
2026-10-19 11:45:51 -04:00
committed by GitHub
parent 9bfbc24c9e
commit 372b420c56
2 changed files with 84 additions and 0 deletions

View File

@@ -6,6 +6,7 @@ import (
"fmt"
"io"
"maps"
"math"
"net"
"regexp"
"strconv"
@@ -807,6 +808,17 @@ func (cmd *Cmd) Float32() (float32, error) {
func toFloat32(val interface{}) (float32, error) {
switch val := val.(type) {
case float64:
// Return the range error that ParseFloat returns for a RESP2 bulk
// string when a RESP3 double does not fit in a float32.
if f := float32(val); math.IsInf(float64(f), 0) && !math.IsInf(val, 0) {
return 0, &strconv.NumError{
Func: "ParseFloat",
Num: strconv.FormatFloat(val, 'g', -1, 64),
Err: strconv.ErrRange,
}
}
return float32(val), nil
case int64:
return float32(val), nil
case string:
@@ -831,6 +843,8 @@ func (cmd *Cmd) Float64() (float64, error) {
func toFloat64(val interface{}) (float64, error) {
switch val := val.(type) {
case float64:
return val, nil
case int64:
return float64(val), nil
case string:

70
command_float_test.go Normal file
View File

@@ -0,0 +1,70 @@
package redis
import (
"context"
"errors"
"math"
"reflect"
"strconv"
"strings"
"testing"
"github.com/redis/go-redis/v9/internal/proto"
)
// RESP3 sends a double reply, for example from ZSCORE, as a float64 value.
// The float helpers of Cmd must accept it, as they accept the RESP2 bulk
// string.
func TestCmdFloatFromDoubleReply(t *testing.T) {
read := func(t *testing.T, reply string) *Cmd {
t.Helper()
cmd := NewCmd(context.Background())
if err := cmd.readReply(proto.NewReader(strings.NewReader(reply))); err != nil {
t.Fatalf("readReply(%q): %v", reply, err)
}
return cmd
}
for _, reply := range []string{",1.5\r\n", "$3\r\n1.5\r\n"} {
cmd := read(t, reply)
if got, err := cmd.Float64(); err != nil || got != 1.5 {
t.Errorf("Float64() for %q = %v, %v; want 1.5, nil", reply, got, err)
}
if got, err := cmd.Float32(); err != nil || got != 1.5 {
t.Errorf("Float32() for %q = %v, %v; want 1.5, nil", reply, got, err)
}
}
for _, reply := range []string{"*2\r\n,1.5\r\n,2\r\n", "*2\r\n$3\r\n1.5\r\n$1\r\n2\r\n"} {
cmd := read(t, reply)
if got, err := cmd.Float64Slice(); err != nil || !reflect.DeepEqual(got, []float64{1.5, 2}) {
t.Errorf("Float64Slice() for %q = %v, %v; want [1.5 2], nil", reply, got, err)
}
if got, err := cmd.Float32Slice(); err != nil || !reflect.DeepEqual(got, []float32{1.5, 2}) {
t.Errorf("Float32Slice() for %q = %v, %v; want [1.5 2], nil", reply, got, err)
}
}
}
// A double outside the float32 range must give the same range error from
// Float32 for a RESP3 double as for a RESP2 bulk string. An infinity sent by
// the server is not an error.
func TestCmdFloat32FromDoubleReplyOutOfRange(t *testing.T) {
for _, reply := range []string{",1e100\r\n", "$5\r\n1e100\r\n"} {
cmd := NewCmd(context.Background())
if err := cmd.readReply(proto.NewReader(strings.NewReader(reply))); err != nil {
t.Fatalf("readReply(%q): %v", reply, err)
}
if got, err := cmd.Float32(); !errors.Is(err, strconv.ErrRange) {
t.Errorf("Float32() for %q = %v, %v; want strconv.ErrRange", reply, got, err)
}
}
cmd := NewCmd(context.Background())
if err := cmd.readReply(proto.NewReader(strings.NewReader(",inf\r\n"))); err != nil {
t.Fatalf("readReply: %v", err)
}
if got, err := cmd.Float32(); err != nil || !math.IsInf(float64(got), 1) {
t.Errorf("Float32() for inf = %v, %v; want +Inf, nil", got, err)
}
}