Skip to content

Cluster slot for String keys ignores byte-level hash tag under a DBCS default charsetΒ #4685

Open

Description

@insaf021

Problem

JedisClusterCRC16.getSlot(String) locates the {hash tag} by scanning for the { and } characters (through JedisClusterHashTag.getHashTag) and only encodes to bytes afterwards. The byte[] overload and the server's keyHashSlot() scan for 0x7B/0x7D in the encoded bytes.

Under a DBCS SafeEncoder.DEFAULT_CHARSET (GBK, Shift_JIS, Big5) a trail byte can be 0x7B/0x7D while the corresponding char is not a brace. The String path then picks a different tag (or none) than the wire bytes and computes a slot the server does not own. The same logical key also routes to two different slots depending on whether the String or the binary API is used.

getSlot(String) backs CommandArguments.getKeyHashSlots(), ClusterCommandObjects key routing and sharded pub/sub, so this affects routing for every keyed command, not just SCAN.

Minimal reproducible example

SafeEncoder.DEFAULT_CHARSET = Charset.forName("GBK");
// GBK bytes: 81 7B 41 81 7D  -> a lead byte + '{' , 'A', a lead byte + '}'
String key = new String(new byte[]{(byte)0x81,0x7B,0x41,(byte)0x81,0x7D}, SafeEncoder.DEFAULT_CHARSET);

JedisClusterCRC16.getSlot(key);                       // 14821  (whole key hashed, no '{' char)
JedisClusterCRC16.getSlot(SafeEncoder.encode(key));   // 16212  (== server keyHashSlot)
  • Single-key ops survive only via a MOVED round-trip.
  • Multi-key commands are mis-grouped: keys the server co-locates are rejected client-side (Cannot get connection for command with multiple hash slots), and keys the client groups get a server-side CROSSSLOT.

Under UTF-8 there is no discrepancy (no multibyte sequence contains 0x7B/0x7D), so a fix is a no-op for the default configuration.

Practical benefit

Non-UTF-8 default charsets are a supported knob (SafeEncoder.DEFAULT_CHARSET, used for GBK/Shift_JIS/Big5 deployments). Making getSlot(String) scan the encoded bytes brings it in line with the binary overload and the server, so hash tags co-locate correctly on those deployments.

Proposed fix

Have getSlot(String) delegate to the byte[] overload so brace scanning happens on the wire bytes: return getSlot(SafeEncoder.encode(key));.

Activity

  1. ggivo commented on Aug 17, 2026

    @ggivo
    Collaborator

    Thanks for reporting it.
    We will triage it as soon as we have free slots.

  2. ggivo commented on Aug 17, 2026

    @ggivo
    Collaborator

    Before we go further, could you share some additional context:

    • How did you find this issue β€” code review, or did it surface in a real deployment?
    • What charset is set as DEFAULT_CHARSET in your environment?
    • How does it manifest: MOVED redirects, a CROSSSLOT error, or silent wrong-data routing?

    A minimal reproducer against an actual Redis Cluster deployment β€” showing the observable failure β€” would help us assess real-world impact and validate the fix end-to-end.

  3. added
    waiting-for-feedbackWe need additional information before we can continue
    and removed on Aug 17, 2026
  4. ggivo commented on Aug 17, 2026

    @ggivo
    Collaborator

    Under a DBCS SafeEncoder.DEFAULT_CHARSET (GBK, Shift_JIS, Big5) a trail byte can be 0x7B/0x7D while the char is not a brace, so the String path picks a different tag (or none) and routes to a slot the server does not own.

    The fix makes Jedis consistent with the server in a configuration that produces incorrect behavior regardless. Jedis agreeing with the server on slot 16212 for ηΈ»AηΈ½ doesn't help the user β€” their key is still being hash-tag-routed on a byte they never intended as a delimiter.

    Seems DBCS encodings are incompatible with Redis Cluster hash tag semantics at the protocol level. The server scans raw bytes for 0x7B/0x7D with no charset awareness, and DBCS trail bytes can collide with those values in ways the client cannot prevent.

    One solution is to use UTF-8 (the default) with Redis Cluster, or use the byte[] API directly.

    If you believe there is a concrete use case not covered here, please reopen with a minimal reproducer showing the exact deployment, the observed failure, and the expected behavior from the user's perspective.

  5. added
    status: declinedA suggestion or change that we don't plan to apply
    and removed
    waiting-for-feedbackWe need additional information before we can continue
    on Aug 17, 2026
  6. added and removed
    status: declinedA suggestion or change that we don't plan to apply
    on Sep 21, 2026
  7. ggivo commented on Sep 21, 2026

    @ggivo
    Collaborator

    After taking a second look, unifying getSlot(String) with getSlot(byte[]) is worth doing on its own, separate from whether DBCS hash tags are advisable.

    Scope: this does not make arbitrary DBCS hash tags safe β€” a trail byte can still form an unintended tag, so the guidance to anchor tags in ASCII (and prefer UTF-8 / the byte[] API for DBCS content) stands. It only guarantees String and byte[] routing agree.

    Today the two overloads return different slots for the same logical key under a DBCS default charset β€” e.g. GBK: getSlot(key)=14821 vs getSlot(SafeEncoder.encode(key))=16212

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions