Repository navigation
ioredisAdapter: set() with options loses this, so setOnlyIfNotExists always throws a TypeError #255
Description
Preflight checklist
- I searched existing issues and didn't find a duplicate.
- This is a bug report, not a usage question.
@fortedigital/nextjs-cache-handler version
3.4.0 (packages/nextjs-cache-handler/src/helpers/ioredisAdapter.ts on master has the same code)
Next.js version
Not relevant: the script below reproduces it with only this package, ioredis and a Redis server. We hit it in a Next.js 16.4.3 production app that uses registerInitialCache.
Summary
When the ioredisAdapter proxy's set is called with an options object, it takes target.set out of the ioredis client and calls it detached:
const setFn = target.set;
return setFn(key, value, ...extraArgs);ioredis' set relies on this, so the call throws TypeError: Cannot read properties of undefined (reading 'options'). Through createRedisHandler this means ctx.setOnlyIfNotExists: true (the non-destructive hydration path added in #181) always throws when the client is wrapped with ioredisAdapter, so registerInitialCache({ setOnlyIfNotExists: true }) cannot be used with ioredis.
Steps to reproduce
import Redis from "ioredis";
import { ioredisAdapter } from "@fortedigital/nextjs-cache-handler/helpers/ioredisAdapter";
import createRedisHandler from "@fortedigital/nextjs-cache-handler/redis-strings";
const client = new Redis("redis://127.0.0.1:6379");
if (client.status !== "ready") await new Promise((r) => client.once("ready", r));
const handler = createRedisHandler({ client: ioredisAdapter(client), keyPrefix: "t:" });
const entry = (body) => ({
value: { kind: "FETCH", data: { headers: {}, body } },
tags: [],
lastModified: Date.now(),
lifespan: { expireAt: Math.floor(Date.now() / 1000) + 600 },
});
await handler.set("k1", entry("x"), {}); // ok
await handler.set("k2", entry("x"), { setOnlyIfNotExists: true }); // TypeErrorExpected vs. actual behavior
Expected: the second call stores k2 only if it does not exist yet (SET ... NX), as it does with the node-redis client.
Actual: TypeError: Cannot read properties of undefined (reading 'options').
What I measured (ioredis 5.11.1, Node 24.19.0, Redis server 8.10.1):
| call | result |
|---|---|
handler.set, no setOnlyIfNotExists, with and without lifespan |
ok |
handler.set, setOnlyIfNotExists: true |
TypeError (with and without lifespan) |
ioredisAdapter(client).set(k, v, { NX: true }) called directly |
TypeError |
ioredisAdapter(client).set(k, v, { EXAT: ... }) called directly |
TypeError |
ioredis alone: const f = client.set; await f("k", "v", "NX") |
the same TypeError, so the cause is the detached method, not this package's argument handling |
I did not look into why the plain and EXAT-only handler.set paths work in my run while the adapter's set throws for EXAT when called directly.
Changing that one line to const setFn = target.set.bind(target); (on a copy of the 3.4.0 file) makes the handler call succeed, and the second setOnlyIfNotExists write does not overwrite the first one (checked against a real Redis).
Router / caching mode in use
App Router + Cache Components (PPR) in our app; the reproduction above does not involve Next.js.
Which handler(s) from this package are configured?
redis-strings via ioredisAdapter
Redis / ioredis / Node.js versions
ioredis 5.11.1, Redis server 8.10.1, Node 24.19.0
Deployment topology
Not relevant to the reproduction (single process, one Redis).
Does this reproduce on the unmodified examples/redis-minimal app?
Haven't tried. The script above exercises only this package, ioredis and Redis.
Contribution
I have not opened a pull request. Before I do: is setOnlyIfNotExists meant to be supported through ioredisAdapter? If so, would you take a PR with the one-line change plus a regression test, and is the isolated Redis e2e suite from #240 the right place for it?
Activity