diff --git a/command.go b/command.go index a843b243..0dfe09a5 100644 --- a/command.go +++ b/command.go @@ -1673,17 +1673,23 @@ func (cmd *UintSliceCmd) readReply(rd *proto.Reader) error { return err } cmd.val = make([]uint64, n) + var elemErr error for i := range cmd.val { switch num, err := rd.ReadUint(); { case err == Nil: cmd.val[i] = 0 case err != nil: - return err + if !isRedisError(err) { + return err + } + if elemErr == nil { + elemErr = err + } default: cmd.val[i] = num } } - return nil + return elemErr } func (cmd *UintSliceCmd) Clone() Cmder { @@ -2104,17 +2110,23 @@ func (cmd *FloatSliceCmd) readReply(rd *proto.Reader) error { } cmd.val = make([]float64, n) + var elemErr error for i := 0; i < len(cmd.val); i++ { switch num, err := rd.ReadFloat(); { case err == Nil: cmd.val[i] = 0 case err != nil: - return err + if !isRedisError(err) { + return err + } + if elemErr == nil { + elemErr = err + } default: cmd.val[i] = num } } - return nil + return elemErr } func (cmd *FloatSliceCmd) Clone() Cmder { @@ -2179,17 +2191,23 @@ func (cmd *StringSliceCmd) readReply(rd *proto.Reader) error { return err } cmd.val = make([]string, n) + var elemErr error for i := 0; i < len(cmd.val); i++ { switch s, err := rd.ReadString(); { case err == Nil: cmd.val[i] = "" case err != nil: - return err + if !isRedisError(err) { + return err + } + if elemErr == nil { + elemErr = err + } default: cmd.val[i] = s } } - return nil + return elemErr } func (cmd *StringSliceCmd) Clone() Cmder { @@ -2446,17 +2464,23 @@ func (cmd *BoolSliceCmd) readReply(rd *proto.Reader) error { return err } cmd.val = make([]bool, n) + var elemErr error for i := 0; i < len(cmd.val); i++ { switch b, err := rd.ReadBool(); { case err == Nil: cmd.val[i] = false case err != nil: - return err + if !isRedisError(err) { + return err + } + if elemErr == nil { + elemErr = err + } default: cmd.val[i] = b } } - return nil + return elemErr } func (cmd *BoolSliceCmd) Clone() Cmder { diff --git a/command_nil_element_desync_test.go b/command_nil_element_desync_test.go index 26ece94c..9993439a 100644 --- a/command_nil_element_desync_test.go +++ b/command_nil_element_desync_test.go @@ -56,6 +56,70 @@ func TestSliceCmdNilElementNoDesync(t *testing.T) { }) } +// An error element in a non-final position is a complete frame, e.g. EVAL with +// `return {'a', redis.error_reply('boom'), 'c'}`. UintSliceCmd, FloatSliceCmd, +// StringSliceCmd and BoolSliceCmd used to return on it with the trailing +// elements still on the wire, so in a pipeline the next command read them as +// its reply. They must consume the whole array and return the first error. +func TestSliceCmdErrorElementNoDesync(t *testing.T) { + t.Run("UintSliceCmd", func(t *testing.T) { + rd := proto.NewReader(strings.NewReader("*3\r\n:255\r\n-ERR item failed\r\n:100\r\n:999\r\n")) + cmd := NewUintSliceCmd(context.Background()) + if err := cmd.readReply(rd); err == nil || err.Error() != "ERR item failed" { + t.Fatalf("readReply err = %v, want ERR item failed", err) + } + if got := cmd.val; len(got) != 3 || got[0] != 255 || got[1] != 0 || got[2] != 100 { + t.Fatalf("unexpected val: %+v", got) + } + assertNextReplyInt(t, rd, 999) + }) + + t.Run("FloatSliceCmd", func(t *testing.T) { + rd := proto.NewReader(strings.NewReader("*3\r\n,1.5\r\n!15\r\nERR item failed\r\n,2.5\r\n:999\r\n")) + cmd := NewFloatSliceCmd(context.Background()) + if err := cmd.readReply(rd); err == nil || err.Error() != "ERR item failed" { + t.Fatalf("readReply err = %v, want ERR item failed", err) + } + if got := cmd.val; len(got) != 3 || got[0] != 1.5 || got[1] != 0 || got[2] != 2.5 { + t.Fatalf("unexpected val: %+v", got) + } + assertNextReplyInt(t, rd, 999) + }) + + t.Run("StringSliceCmd", func(t *testing.T) { + rd := proto.NewReader(strings.NewReader("*4\r\n$1\r\na\r\n-ERR first\r\n-ERR second\r\n$1\r\nc\r\n:999\r\n")) + cmd := NewStringSliceCmd(context.Background()) + if err := cmd.readReply(rd); err == nil || err.Error() != "ERR first" { + t.Fatalf("readReply err = %v, want ERR first", err) + } + if got := cmd.val; len(got) != 4 || got[0] != "a" || got[1] != "" || got[2] != "" || got[3] != "c" { + t.Fatalf("unexpected val: %+v", got) + } + assertNextReplyInt(t, rd, 999) + }) + + t.Run("BoolSliceCmd", func(t *testing.T) { + rd := proto.NewReader(strings.NewReader("*3\r\n:1\r\n-ERR item failed\r\n:1\r\n:999\r\n")) + cmd := NewBoolSliceCmd(context.Background()) + if err := cmd.readReply(rd); err == nil || err.Error() != "ERR item failed" { + t.Fatalf("readReply err = %v, want ERR item failed", err) + } + if got := cmd.val; len(got) != 3 || !got[0] || got[1] || !got[2] { + t.Fatalf("unexpected val: %+v", got) + } + assertNextReplyInt(t, rd, 999) + }) + + t.Run("TruncatedReply", func(t *testing.T) { + rd := proto.NewReader(strings.NewReader("*3\r\n$1\r\na\r\n-ERR item failed\r\n")) + cmd := NewStringSliceCmd(context.Background()) + err := cmd.readReply(rd) + if err == nil || isRedisError(err) { + t.Fatalf("readReply err = %v, want the read error", err) + } + }) +} + func assertNextReplyInt(t *testing.T, rd *proto.Reader, want int64) { t.Helper() got, err := rd.ReadInt()