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

  1. Always attach a TTL: SetCommand.set(key, value).ex(Duration.ofSeconds(60)) before calling setEX
  2. If the TTL is genuinely optional, branch: use setEX when a TTL exists and the plain set() overload when it does not
  3. 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

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


AI-assisted analysis of redisson/redisson@91188987c2 (2026-08-14). Data as JSON: /api/errors/1af4b60982b1ab93. Report an issue: GitHub.