fastify / fastify-caching

A Fastify plugin to facilitate working with cache headers
MIT License
204 stars 26 forks source link

When using `cache.get` and `cache.set` within the same route handler, a `Reply was already sent` error occurs #153

Closed 5t111111 closed 1 month ago

5t111111 commented 1 month ago

Prerequisites

Issue

Sorry, I'm not sure if this is a bug or if I'm using it incorrectly.

I am trying to use fastify-caching to cache the results of a process that fetches data from an external API within a route handler.

The mechanism is simple:

To achieve this, I tried to implement caching by nesting cache.get and cache.set callbacks, as shown below. However, I always get a Reply was already sent error when there is no cached data. Also, the response being sent is not the externalApiResponseJson, but an empty response.

import Fastify from "fastify";
import fastifyCaching from "@fastify/caching";

const fastify = Fastify({
  logger: true,
});

fastify.register(fastifyCaching, {
  privacy: fastifyCaching.privacy.PRIVATE,
});

fastify.get("/", async function (request, reply) {
  fastify.cache.get(
    "extData",
    // Make this callback async to use await in it.
    async (err, result) => {
      if (!err && result && result.item) {
        reply.send(result.item);
      } else {
        const externalApiResponse = await fetch(
          "https://jsonplaceholder.typicode.com/todos/1"
        );
        const externalApiResponseJson = await externalApiResponse.json();

        fastify.cache.set("extData", externalApiResponseJson, 60000, (err) => {
          if (err) {
            throw err;
          }
          // ** Every time you send reply here, "Reply was already sent" error occurs. **
          reply.send(externalApiResponseJson);
        });
      }
    }
  );
});

fastify.listen({ port: 3000 }, function (err, address) {
  if (err) {
    fastify.log.error(err);
    process.exit(1);
  }
});

This code is also a minimal reproduction of the issue, and the actual project involves more complex processing, but the basic structure remains the same.

I looked through the documentation and test codes of fastify-caching, but I could not find an example that uses cache.get and cache.set within the same route handler, so I am unsure of the correct usage. I would appreciate any guidance.

Node version: 20.18.0

package.json

{
  "name": "fastify-caching-issue",
  "main": "index.js",
  "dependencies": {
    "@fastify/caching": "^9.0.1",
    "fastify": "^5.0.0"
  }
}
5t111111 commented 1 month ago

In my situation, the issue was resolved by explicitly using the useAwait option. I believe the problem was likely caused by the callback function of cache.set not being designed to work with async / await, so I think my usage was incorrect.

Below is the updated code:

import Fastify from "fastify";
import fastifyCaching from "@fastify/caching";
import abstractCache from "abstract-cache";

const fastify = Fastify({
  logger: true,
});

const abcache = abstractCache({
  useAwait: true,
});

fastify.register(fastifyCaching, {
  privacy: fastifyCaching.privacy.PRIVATE,
  cache: abcache,
});

fastify.get("/", async function (request, reply) {
  const extData = await fastify.cache.get("extData");
  if (extData) {
    return reply.send(extData.item);
  }

  const externalApiResponse = await fetch(
    "https://jsonplaceholder.typicode.com/todos/1"
  );
  const externalApiResponseJson = await externalApiResponse.json();

  fastify.cache.set("extData", externalApiResponseJson, 10000);

  reply.send(externalApiResponseJson);
});

fastify.listen({ port: 3000 }, function (err, address) {
  if (err) {
    fastify.log.error(err);
    process.exit(1);
  }
});