Search code examples
file-uploadcloudinaryfastify

Why does Fastify send a response and doesn't wait for my response


I'm new to Fastify and backend development. I'm trying to upload an image to Cloudinary using my Fastify server for the first time. The upload_stream method takes two arguments. The first one is an options object, the second is a callback function that must be executed after the image is uploaded.

async function uploadImage(request: FastifyRequest, reply: FastifyReply) {
    const pipeline = util.promisify(stream.pipeline);

    const data = await request.file();

    if(!data) {
        return reply.code(400).send("no data")
    }

    const randomFilename = crypto.randomBytes(30).toString('hex');

    await pipeline(
        data.file,
        server.cloudinary.uploader.upload_stream(
            { public_id: randomFilename, folder: 'library' },
            (error, result) => {
                if(result) {
                    console.log(result);
                    reply.code(201).send({
                        meta: {
                            code: 201,
                            message: result?.url
                        }
                    });
                } else {
                    reply.code(400).send({
                        meta: {
                            code: 400,
                            message: error
                        }
                    });
                }
            })
    );
}

The image loads to Cloudinary and console.log(result); in a callback function prints a result object in my console. That's fine. But when I'm trying to pass an uploaded image's link to the response object, it says that the reply has already been sent with a status code 200. The full error message is below. As you migh see, I don't even have a response with status code 200 in my code.

WARN (17040): Reply already sent
    reqId: "req-1"
    err: {
      "type": "FastifyError",
      "message": "Reply was already sent.",
      "stack":
          FastifyError: Reply was already sent.
              at _Reply.Reply.send (C:\...\fastify-test\node_modules\fastify\lib\reply.js:127:26)
              at C:\...\fastify-test\src\api\user\controller.ts:133:29
              at C:\...\fastify-test\node_modules\cloudinary\lib\utils\index.js:1275:12
              at IncomingMessage.<anonymous> (C:\...\fastify-test\node_modules\cloudinary\lib\uploader.js:509:9)
              at IncomingMessage.emit (node:events:525:35)
              at IncomingMessage.emit (node:domain:489:12)
              at endReadableNT (node:internal/streams/readable:1359:12)
              at processTicksAndRejections (node:internal/process/task_queues:82:21)
      "name": "FastifyError",
      "code": "FST_ERR_REP_ALREADY_SENT",
      "statusCode": 500
    }

The question is how to send a link of an uploaded image as a server response.


Solution

  • The issue is the async handler.

    Since it is an async function, what it returns is sent as the HTTP response. In fact, if you add return 'foo' after the await pipeline() I would expect your client will get that string.

    What is happening is that the pipeline function ends before the cloudinary.uploader.upload_stream function executes the callback. Welcome to Node.js and streams 😈

    So the uploadImage returns undefined and the callback that handle the reply object runs after the response has been sent.

    There are plenty some solution here

    1. Add return reply after the pipeline execution - this will force fastify to not submit the handler results
    await pipeline(..)
    return reply
    
    1. Add a pending promise:
     await new Promise((resolve, reject) => {
      pipeline(
            data.file,
            server.cloudinary.uploader.upload_stream(
                { public_id: randomFilename, folder: 'library' },
                (error, result) => {
                    if(result) {
                        console.log(result);
                        reply.code(201).send({
                            meta: {
                                code: 201,
                                message: result?.url
                            }
                        });
                        resolve()
                    } else {
                        reply.code(400).send({
                            meta: {
                                code: 400,
                                message: error
                            }
                        });
                        reject()
                    }
                })
        );
    })
    
    

    Further reading: https://www.fastify.io/docs/latest/Reference/Routes/#async-await

    This is explained even more on my Fastify book - checkout my profile for more info 😄