redisson/redisson · error · IllegalArgumentException
Expiration must not be null!
Error message
Expiration must not be null!
What it means
SETEX requires exactly three arguments: key, seconds, value. RedissonReactiveStringCommands.setEX maps Spring Data SetCommand to SETEX and validates that an expiration is present; without it the command cannot be serialized, so it throws IllegalArgumentException('Expiration must not be null!') before any I/O happens. The check mirrors SETEX's signature — the TTL is not optional.
Source
Thrown at redisson-spring/redisson-spring-data/redisson-spring-data-25/src/main/java/org/redisson/spring/data/connection/RedissonReactiveStringCommands.java:173
byte[] keyBuf = toByteArray(command.getKey());
byte[] valueBuf = toByteArray(command.getValue());
Mono<Boolean> m = write(keyBuf, StringCodec.INSTANCE, RedisCommands.SETNX, keyBuf, valueBuf);
return m.map(v -> new BooleanResponse<>(command, v));
});
}
private static final RedisCommand<Boolean> SETEX = new RedisCommand<Boolean>("SETEX", new BooleanReplayConvertor());
@Override
public Flux<BooleanResponse<SetCommand>> setEX(Publisher<SetCommand> commands) {
return execute(commands, command -> {
Assert.notNull(command.getKey(), "Key must not be null!");
Assert.notNull(command.getValue(), "Value must not be null!");
if (!command.getExpiration().isPresent()) {
throw new IllegalArgumentException("Expiration must not be null!");
}
byte[] keyBuf = toByteArray(command.getKey());
byte[] valueBuf = toByteArray(command.getValue());
Mono<Boolean> m = write(keyBuf, StringCodec.INSTANCE, SETEX,
keyBuf, command.getExpiration().get().getExpirationTimeInSeconds(), valueBuf);
return m.map(v -> new BooleanResponse<>(command, v));
});
}
private static final RedisCommand<String> PSETEX = new RedisCommand<String>("PSETEX");
@Override
public Flux<BooleanResponse<SetCommand>> pSetEX(Publisher<SetCommand> commands) {
return execute(commands, command -> {
Assert.notNull(command.getKey(), "Key must not be null!");View on GitHub (pinned to 91188987c2)
Solutions
- Always attach a TTL: SetCommand.set(key, value).ex(Duration.ofSeconds(60)) before calling setEX
- If the TTL is genuinely optional, branch: use setEX when a TTL exists and the plain set() overload when it does not
- Note SETEX is deprecated in Redis >= 2.6.12; prefer set() with .ex(...) for new code
Example fix
// before
SetCommand cmd = SetCommand.set(key, value); // no expiration
reactiveStringCommands.setEX(Flux.just(cmd)); // throws
// after
SetCommand cmd = SetCommand.set(key, value)
.ex(Duration.ofSeconds(60));
reactiveStringCommands.setEX(Flux.just(cmd)); Defensive patterns
Strategy: validation
Validate before calling
// Ensure expiration before setEX: SetCommand cmd = SetCommand.set(key, value); Duration ttl = optionalTtl != null ? optionalTtl : Duration.ofSeconds(60); cmd = cmd.ex(ttl); reactiveStringCommands.setEX(Flux.just(cmd));
Prevention
- Always call .ex(Duration) on SetCommand before setEX/pSetEX
- Branch on TTL presence: setEX when present, plain set() when not
- Prefer the modern set() overload with expiration — SETEX is legacy
When it happens
Trigger: Calling reactiveStringCommands.setEX(...) with SetCommand.set(key, value) that never had .ex(Duration) applied; or building the SetCommand with a null Expiration. Reached via ReactiveStringCommands or reactive RedisTemplate helpers that dispatch on the presence of a TTL.
Common situations: Conditional TTL logic (e.g. 'persist if flag unset') where the code path forgets to attach .ex(); refactoring where an expiration field becomes optional; copy-paste from the plain set() path where expiration is legal to omit.
Related errors
- Expiration must not be null!
- Command must not define expiration nor option for GETSET.
- Unable to close JNDI context
- Expiration must not be null!
- Command must not define expiration nor option for GETSET.
AI-assisted analysis of redisson/redisson@91188987c2 (2026-08-14).
Data as JSON: /api/errors/1af4b60982b1ab93.
Report an issue: GitHub.