Skip to content

ioredisAdapter: set() with options loses this, so setOnlyIfNotExists always throws a TypeError #255

Open

Description

@bolechen

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 });  // TypeError

Expected 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions