Mongoose/Express NodeJS API only sending one image back to client

41 Views Asked by At

I have a buffer array and I want to convert it into images and send it to the user. The problem is that this code currently only sends one image:

const express = require("express");
const asyncHandler = require("express-async-handler");
const Image = require('../models/Image');
const router = express.Router();
const multer = require('multer');
const sharp = require('sharp');

router.get("/", asyncHandler(async (req, res) => {
  const imagesInfo = await Image.find();
  const imagesBuffers = imagesInfo.map((img) => img.data);

  const promises = imagesBuffers.map((buffer) =>
    sharp(buffer).toFormat('jpeg').toBuffer()
  );
  Promise.all(promises)
    .then((outputBuffers) => {
      res.contentType('image/jpeg');
      outputBuffers.forEach((outputBuffer) => {
        res.write(outputBuffer, 'binary');
      });

      res.end();
    })
    .catch((err) => {
      console.error('Error:', err);
      res.status(500).send('Error.');
    });
}));
module.exports = router;

../models/Image is defined as:

const mongoose = require("mongoose");
const ImageSchema = new mongoose.Schema({
  filename: { type: String, },
  data: { type: Buffer, },
}, { timestamps: true });
const Image = mongoose.model("Image", ImageSchema);
module.exports = Image;

How can I make it send the total number of images?

1

There are 1 best solutions below

0
On

The issue with your current implementation is that you're attempting to send multiple images in a single HTTP response, which isn't directly supported by HTTP. Each HTTP response is meant for a single resource. There are a couple different ways we can circumvent this for your particular use case.

Convert Image to Base64

The server can send the images back to the client in an array of base64 strings, like so:

router.get("/", asyncHandler(async (req, res) => {
  const imagesInfo = await Image.find();
  
  const promises = imagesInfo.map((img) => 
    sharp(img.data).toFormat('jpeg').toBuffer()
  );

  Promise.all(promises)
    .then((outputBuffers) => {
      const base64Images = outputBuffers.map(buffer => buffer.toString('base64'));
      res.json(base64Images);
    })
    .catch((err) => {
      console.error('Error:', err);
      res.status(500).send('Error.');
    });
}));

And this is an example of how the client could handle the response:

<div id="container"></div>

<script>
  fetch('http://yourserver.com/')
    .then(response => response.json())
    .then(images => {
      const container = document.getElementById('container');
      
      images.forEach(base64Image => {
        const img = document.createElement('img');
        img.src = 'data:image/jpeg;base64,' + base64Image;
        container.appendChild(img);
      });
    })
    .catch(error => console.error('Error:', error));
</script>

The issue with this method is that converting the images to base64 will increases the size of the data by approximately 33%, which is an issue if performance is important to your task.

Send Back URLs

An alternative solution is to just send back the image URL to the client, which ususally works the same way depending on what your client is trying to accomplish. Since your images should have unique file names, we can do this like so:

router.get("/", asyncHandler(async (req, res) => {
  const imagesInfo = await Image.find();
  const imageUrls = imagesInfo.map((img) => `http://yourserver.com/images/${img.filename}`);
  res.json(imageUrls);
}));

// Endpoint to serve an individual image
router.get("/images/:filename", asyncHandler(async (req, res) => {
  try {
    const filename = req.params.filename;
    const imageInfo = await Image.findOne({ filename: filename });

    if (imageInfo) {
      const buffer = imageInfo.data;
      const image = await sharp(buffer).toFormat('jpeg').toBuffer();
      res.contentType('image/jpeg');
      res.send(image);
    } else {
      res.status(404).send('Image not found.');
    }
  } catch (err) {
    console.error('Error:', err);
    res.status(500).send('Error.');
  }
}));

But if you need the raw data for some reason, this method wouldn't be suitable OOTB, you would need to get the raw data from each individual request on the returned list of URLs from the initial request.