Summary
When using RedisConnection.execute(String command, byte[]... args) to call Redis module commands that return a non-bulk-string RESP response (e.g. integer replies), Lettuce's internal ByteArrayOutput throws:
java.lang.UnsupportedOperationException:
io.lettuce.core.output.ByteArrayOutput does not support set(long)
Commands returning bulk strings or status replies (e.g. JSON.GET, JSON.SET) work correctly through the same API. Integer-returning commands fail.
Environment
- Spring Boot: 4.0 pulled in Spring boot starter data redis dependency
- Redis Stack Server: latest so 7.x with RedisJSON module
- Java: 25
Steps to Reproduce
I have a service that wraps RedisJSON commands using StringRedisTemplate and RedisCallback:
@RequiredArgsConstructor
class RedisJsonExecutor {
private final StringRedisTemplate redisTemplate;
/**
* Works for JSON.SET (returns "+OK")
* and JSON.GET (returns bulk string).
*/
String executeForString(String command, String... args) {
return redisTemplate.execute((RedisCallback<String>) connection -> {
byte[][] rawArgs = new byte[args.length][];
for (int i = 0; i < args.length; i++) {
rawArgs[i] =
args[i].getBytes(StandardCharsets.UTF_8);
}
Object result =
connection.execute(command, rawArgs);
if (result == null) return null;
if (result instanceof byte[] bytes) {
return new String(
bytes,
StandardCharsets.UTF_8
);
}
return result.toString();
});
}
}
Using this for JSON.SET and JSON.GET works correctly:
// ✅ Works — JSON.SET returns "+OK"
executor.executeForString(
"JSON.SET",
key,
"$",
"{\"name\":\"test\"}"
);
// ✅ Works — JSON.GET returns bulk string
String json = executor.executeForString(
"JSON.GET",
key,
"$"
);
Using the same approach for JSON.DEL fails:
// ❌ Fails — JSON.DEL returns integer (:1)
executor.executeForString(
"JSON.DEL",
key,
"$.some.path"
);
Actual Behaviour
Calling:
connection.execute("JSON.DEL", args)
throws:
org.springframework.data.redis.RedisSystemException:
Unknown redis exception
Caused by:
java.lang.UnsupportedOperationException:
io.lettuce.core.output.ByteArrayOutput
does not support set(long)
Stack trace:
org.springframework.data.redis.RedisSystemException:
Unknown redis exception
at o.s.d.r.FallbackExceptionTranslationStrategy.getFallback(FallbackExceptionTranslationStrategy.java:49)
at o.s.d.r.connection.lettuce.LettuceConnection.convertLettuceAccessException(LettuceConnection.java:313)
at o.s.d.r.connection.lettuce.LettuceConnection.await(LettuceConnection.java:1015)
at o.s.d.r.connection.lettuce.LettuceConnection.execute(LettuceConnection.java:351)
at o.s.d.r.connection.lettuce.LettuceConnection.execute(LettuceConnection.java:318)
Caused by:
java.lang.UnsupportedOperationException:
io.lettuce.core.output.ByteArrayOutput
does not support set(long)
at io.lettuce.core.output.CommandOutput.set(CommandOutput.java:112)
at io.lettuce.core.protocol.RedisStateMachine.safeSet(RedisStateMachine.java:783)
at io.lettuce.core.protocol.RedisStateMachine.handleInteger(RedisStateMachine.java:434)
at io.lettuce.core.protocol.RedisStateMachine$State$Type.handle(RedisStateMachine.java:211)
at io.lettuce.core.protocol.RedisStateMachine.doDecode(RedisStateMachine.java:364)
Possible root Cause
LettuceConnection.execute(String command, byte[]... args) resolves the expected CommandOutput using internal type hints:
@Override
public Object execute(String command, byte[]... args) {
return execute(command, null, args);
}
public Object execute(String command,
@Nullable CommandOutput commandOutputTypeHint,
byte[]... args) {
CommandOutput expectedOutput =
commandOutputTypeHint != null
? commandOutputTypeHint
: typeHints.getTypeHint(commandType);
...
}
For unknown/module commands, TypeHints falls back to ByteArrayOutput:
public CommandOutput getTypeHint(ProtocolKeyword type) {
return getTypeHint(type, new ByteArrayOutput<>(CODEC));
}
This works for commands returning bulk strings or status replies, but fails for commands returning integer RESP replies.
Examples:
JSON.GET → bulk string → works
JSON.SET → status reply → works
JSON.DEL → integer reply → fails
This is not a Lettuce bug itself. ByteArrayOutput intentionally does not support integer responses, so when Redis returns an integer reply, Lettuce correctly fails with:
UnsupportedOperationException:
ByteArrayOutput does not support set(long)
The main limitation is that Spring Data Redis does not expose a way to specify the expected CommandOutput type through the RedisConnection abstraction.
Suggestion
Possible improvements:
Option 1 — Expose typed execute overload on RedisConnection
LettuceConnection already supports:
execute(String command,
CommandOutput commandOutputTypeHint,
byte[]... args)
Exposing this overload through RedisConnection would allow callers to provide the correct output type for module/custom commands.
Option 2 — Expose RedisJSON commands through Spring abstractions
Expose RedisJSON commands (e.g. via RedisJsonCommands) through the Spring Data Redis abstraction layer so module commands can use the correct typed outputs internally instead of relying on generic command execution.
I'd be happy to contribute a PR for this if the team agrees on the approach.
Workaround
I had to bypass connection.execute() entirely and access the native Lettuce connection to use dispatch() with IntegerOutput:
@SuppressWarnings({"unchecked", "deprecation"})
long executeForLong(String command, String... args) {
return redisTemplate.execute((RedisCallback<Long>) connection -> {
Object nativeConn =
connection.getNativeConnection();
// getNativeConnection() may return either
// StatefulRedisConnection or RedisAsyncCommandsImpl
// depending on pooling configuration
StatefulRedisConnection<byte[], byte[]> stateful =
switch (nativeConn) {
case StatefulRedisConnection<?, ?> s ->
(StatefulRedisConnection<byte[], byte[]>) s;
case io.lettuce.core.api.async.RedisAsyncCommands<?, ?> async ->
(StatefulRedisConnection<byte[], byte[]>)
async.getStatefulConnection();
default -> throw new IllegalStateException(
"Unsupported native connection type: "
+ nativeConn.getClass().getName());
};
var commands = stateful.sync();
CommandArgs<byte[], byte[]> cmdArgs =
new CommandArgs<>(ByteArrayCodec.INSTANCE);
for (String arg : args) {
cmdArgs.add(
arg.getBytes(StandardCharsets.UTF_8)
);
}
return commands.dispatch(
asCommandType(command),
new IntegerOutput<>(ByteArrayCodec.INSTANCE),
cmdArgs
);
});
}
private ProtocolKeyword asCommandType(String command) {
return new ProtocolKeyword() {
@Override
public byte[] getBytes() {
return command.getBytes(StandardCharsets.UTF_8);
}
@Override
public String name() {
return command;
}
};
}
This workaround requires:
- reaching into Lettuce internals via
getNativeConnection()
- handling multiple possible native connection types
- using deprecated
RedisAsyncCommands.getStatefulConnection()
- constructing a custom
ProtocolKeyword for module commands not present in CommandType
Summary
When using
RedisConnection.execute(String command, byte[]... args)to call Redis module commands that return a non-bulk-string RESP response (e.g. integer replies), Lettuce's internalByteArrayOutputthrows:Commands returning bulk strings or status replies (e.g.
JSON.GET,JSON.SET) work correctly through the same API. Integer-returning commands fail.Environment
Steps to Reproduce
I have a service that wraps RedisJSON commands using
StringRedisTemplateandRedisCallback:Using this for
JSON.SETandJSON.GETworks correctly:Using the same approach for
JSON.DELfails:Actual Behaviour
Calling:
throws:
Stack trace:
Possible root Cause
LettuceConnection.execute(String command, byte[]... args)resolves the expectedCommandOutputusing internal type hints:For unknown/module commands,
TypeHintsfalls back toByteArrayOutput:This works for commands returning bulk strings or status replies, but fails for commands returning integer RESP replies.
Examples:
JSON.GET→ bulk string → worksJSON.SET→ status reply → worksJSON.DEL→ integer reply → failsThis is not a Lettuce bug itself.
ByteArrayOutputintentionally does not support integer responses, so when Redis returns an integer reply, Lettuce correctly fails with:The main limitation is that Spring Data Redis does not expose a way to specify the expected
CommandOutputtype through theRedisConnectionabstraction.Suggestion
Possible improvements:
Option 1 — Expose typed execute overload on RedisConnection
LettuceConnectionalready supports:Exposing this overload through
RedisConnectionwould allow callers to provide the correct output type for module/custom commands.Option 2 — Expose RedisJSON commands through Spring abstractions
Expose RedisJSON commands (e.g. via
RedisJsonCommands) through the Spring Data Redis abstraction layer so module commands can use the correct typed outputs internally instead of relying on generic command execution.I'd be happy to contribute a PR for this if the team agrees on the approach.
Workaround
I had to bypass
connection.execute()entirely and access the native Lettuce connection to usedispatch()withIntegerOutput:This workaround requires:
getNativeConnection()RedisAsyncCommands.getStatefulConnection()ProtocolKeywordfor module commands not present inCommandType