go-redis/redis · error
redis: invalid cursor ID type
Error message
redis: invalid cursor ID type
What it means
Returned internally by ClusterClient.executeCursorCommand when the 4th argument (the cursor ID, args[3]) is not a string. The router casts args[3] to string to compute hashtag.Slot for sticky routing. The public FTCursorRead/FTCursorDel helpers pass an int cursorId which go-redis serializes to a string arg, so this guard catches a Cmder whose cursorId argument was constructed as a non-string type at a low level.
Source
Thrown at osscluster_router.go:19
package redis
import (
"context"
"errors"
"fmt"
"reflect"
"sync"
"time"
"github.com/redis/go-redis/v9/internal/hashtag"
"github.com/redis/go-redis/v9/internal/routing"
)
var (
errInvalidCmdPointer = errors.New("redis: invalid command pointer")
errNoCmdsToAggregate = errors.New("redis: no commands to aggregate")
errNoResToAggregate = errors.New("redis: no results to aggregate")
errInvalidCursorCmdArgsCount = errors.New("redis: FT.CURSOR command requires at least 3 arguments")
errInvalidCursorIdType = errors.New("redis: invalid cursor ID type")
)
// slotResult represents the result of executing a command on a specific slot
type slotResult struct {
cmd Cmder
keys []string
err error
}
// routeAndRun routes a command to the appropriate cluster nodes and executes it
func (c *ClusterClient) routeAndRun(ctx context.Context, cmd Cmder, node *clusterNode) error {
var policy *routing.CommandPolicy
if c.cmdInfoResolver != nil {
policy = c.cmdInfoResolver.GetCommandPolicy(ctx, cmd)
}
// Set stepCount from cmdInfo if not already setView on GitHub (pinned to 36d97525cd)
Solutions
- Use FTCursorRead(ctx, index, cursorId, count) or FTCursorDel(ctx, index, cursorId) so the cursor ID is serialized correctly.
- If constructing the Cmder manually, pass the cursor ID as a string (e.g. strconv.Itoa(cursorId)) in args[3].
- Verify no intermediate layer is mutating command argument types before the cluster router sees them.
Example fix
// before — cursor ID passed as a non-string at a low level cmd := redis.NewStatusCmd(ctx, "FT.CURSOR", "READ", "idx", 42) // int, not string-serialized // after — use the typed helper (serializes cursorId correctly) cmd := client.FTCursorRead(ctx, "idx", 42, 10)
Defensive patterns
Strategy: validation
Validate before calling
func validateFTCursorId(args []interface{}) error {
if len(args) < 4 {
return errors.New("not enough args")
}
if _, ok := args[3].(string); !ok {
return fmt.Errorf("cursor ID at args[3] must be a string, got %T", args[3])
}
return nil
} Prevention
- Use the typed FTCursorRead/FTCursorDel helpers so the cursor ID is serialized correctly.
- When building FT.CURSOR Cmders manually, pass the cursor ID as a string.
- Ensure no marshalling layer mutates argument types before the cluster router sees them.
When it happens
Trigger: A manually-constructed FT.CURSOR Cmder where args[3] is a non-string type (e.g. an int passed directly without serialization, or a nil). A custom command path that bypasses the normal arg serialization. Not produced by the documented FTCursorRead/FTCursorDel builders.
Common situations: Custom interop code building FT.CURSOR commands with raw int cursor IDs instead of letting the builder serialize them. Marshalling/buffering layers that change argument types. Bugs in test harnesses that hand-craft Cmder args.
Related errors
- redis: FT.CURSOR command requires at least 3 arguments
- redis: tx pipeline produced no outcome
- redis: no valid results to aggregate for max operation
- redis: no valid results to aggregate for logical AND operati
- redis: no valid results to aggregate for logical OR operatio
AI-assisted analysis of go-redis/redis@36d97525cd (2026-08-06).
Data as JSON: /data/errors/75f0b59ec89bb9ea.json.
Report an issue: GitHub.