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 set

View on GitHub (pinned to 36d97525cd)

Solutions

  1. Use FTCursorRead(ctx, index, cursorId, count) or FTCursorDel(ctx, index, cursorId) so the cursor ID is serialized correctly.
  2. If constructing the Cmder manually, pass the cursor ID as a string (e.g. strconv.Itoa(cursorId)) in args[3].
  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

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


AI-assisted analysis of go-redis/redis@36d97525cd (2026-08-06). Data as JSON: /data/errors/75f0b59ec89bb9ea.json. Report an issue: GitHub.