RedisKvStore.set() passes options.ttl.total("second") straight to Redis SETEX, which requires a whole number of seconds. Any Temporal.Duration that is not an exact number of seconds is rejected by the server, so the write fails instead of storing the value with a rounded expiry.
This is reachable from application code: KvStoreSetOptions.ttl is public API and accepts any Temporal.Duration. It also became easier to hit in 2.4, where FederationOptions.publicKeyTtl and FederationOptions.httpMessageSignaturesSpecTtl let an application choose the cache lifetimes directly (#1027). Fedify's own defaults are all whole seconds, so nothing in the library trips it today.
Environment
@fedify/redis 2.4.0 (main, commit 621958b)
- Deno 2.9.5, macOS 26.6.2 (aarch64)
- Redis 7 (
redis:7-alpine in Docker)
Steps to reproduce
import { RedisKvStore } from "@fedify/redis/kv";
import { Redis } from "ioredis";
const store = new RedisKvStore(new Redis("redis://127.0.0.1:6379"));
await store.set(["k"], "v", { ttl: Temporal.Duration.from({ seconds: 1 }) });
await store.set(["k"], "v", { ttl: Temporal.Duration.from({ milliseconds: 500 }) });
Expected behavior
Both writes store the value. A sub-second TTL is expected to be normalized to the smallest expiry Redis can express rather than rejected.
Actual behavior
The first write succeeds. The second throws ReplyError: ERR value is not an integer or out of range.
The failure is not limited to sub-second durations. Every duration whose total is not an integer number of seconds fails the same way, and a zero duration fails with a different message:
| TTL |
ttl.total("second") |
Result |
| 1 second |
1 |
stored |
| 1500 milliseconds |
1.5 |
ERR value is not an integer or out of range |
| 500 milliseconds |
0.5 |
ERR value is not an integer or out of range |
| 1 millisecond |
0.001 |
ERR value is not an integer or out of range |
| 0 |
0 |
ERR invalid expire time in 'setex' command |
Suggested fix
Normalize the argument to a positive integer before calling SETEX, so that a sub-second TTL becomes one second rather than an error. RedisKvStore has no cas() method, so set() is the only affected call site.
The adapter tests are gated on REDIS_URL and skip entirely without a live server, so a regression test for this needs a Redis instance to be meaningful.
AI usage disclosure
Per AI_POLICY.md: this report was AI-assisted. The problem was first raised by CodeRabbit on #1027; Claude Code drafted this report and ran the reproduction above against a local Redis 7 container. I checked the reproduction output and edited the text before filing.
RedisKvStore.set()passesoptions.ttl.total("second")straight to RedisSETEX, which requires a whole number of seconds. AnyTemporal.Durationthat is not an exact number of seconds is rejected by the server, so the write fails instead of storing the value with a rounded expiry.This is reachable from application code:
KvStoreSetOptions.ttlis public API and accepts anyTemporal.Duration. It also became easier to hit in 2.4, whereFederationOptions.publicKeyTtlandFederationOptions.httpMessageSignaturesSpecTtllet an application choose the cache lifetimes directly (#1027). Fedify's own defaults are all whole seconds, so nothing in the library trips it today.Environment
@fedify/redis2.4.0 (main, commit 621958b)redis:7-alpinein Docker)Steps to reproduce
Expected behavior
Both writes store the value. A sub-second TTL is expected to be normalized to the smallest expiry Redis can express rather than rejected.
Actual behavior
The first write succeeds. The second throws
ReplyError: ERR value is not an integer or out of range.The failure is not limited to sub-second durations. Every duration whose total is not an integer number of seconds fails the same way, and a zero duration fails with a different message:
ttl.total("second")11.5ERR value is not an integer or out of range0.5ERR value is not an integer or out of range0.001ERR value is not an integer or out of range0ERR invalid expire time in 'setex' commandSuggested fix
Normalize the argument to a positive integer before calling
SETEX, so that a sub-second TTL becomes one second rather than an error.RedisKvStorehas nocas()method, soset()is the only affected call site.The adapter tests are gated on
REDIS_URLand skip entirely without a live server, so a regression test for this needs a Redis instance to be meaningful.AI usage disclosure
Per AI_POLICY.md: this report was AI-assisted. The problem was first raised by CodeRabbit on #1027; Claude Code drafted this report and ran the reproduction above against a local Redis 7 container. I checked the reproduction output and edited the text before filing.