1

I am using the following code to first download a file, and then send a response to the client:


async function download(uri, filename, callback) {
  request.head(uri, async function (err, res, body) {
    console.log("content-type:", res.headers["content-type"]);
    console.log("content-length:", res.headers["content-length"]);

    request(uri).pipe(fs.createWriteStream(filename)).on("close", callback);
  });
}

async function ytsSearch(name) {
  const movies = await yts.listMovies({ limit: 3, query_term: name });
  return movies;
}

async function searchAndDownload(name) {
  movies = await ytsSearch(name);
  movies.movies.forEach(async function (movie) {
    if (
      fs.existsSync(
        path.join(__dirname, "covers", movie.title_english + ".png")
      )
    ) {
      return;
    } else {
      await download(
        movie.medium_cover_image,
        `covers/${movie.title_english}.png`,
        function () {
          console.log("done");
        }
      );
    }
  });
  return movies;
}

app.use("/movie", function (req, res) {
  const { name } = req.body;
  searchAndDownload(name).then((movies) => console.log(movies));
});

However, On the client side, The response is received first, before the file is saved on the server. I know I am probably doing something wrong with so many asynchronous functions, but can't seem to find out what exactly is going wrong.

Output:
enter image description here

As you can see, the response is sent first, and the picture is downloaded second.
Minimal Reproducible Code:

const express = require("express");
const app = express();
const http = require("http");
const request = require("request");
const server = http.createServer(app);
const cors = require("cors");
const yts = require("yts");
const fs = require("fs");
const path = require("path");

const port =5000

async function download(uri, filename, callback) {
  request.head(uri, async function (err, res, body) {
    console.log("content-type:", res.headers["content-type"]);
    console.log("content-length:", res.headers["content-length"]);

    request(uri).pipe(fs.createWriteStream(filename)).on("close", callback);
  });
}

async function ytsSearch(name) {
  const movies = await yts.listMovies({ limit: 3, query_term: name });
  return movies;
}

async function searchAndDownload() {
  movies = await ytsSearch("minions");
  movies.movies.forEach(async function (movie) {
    if (
      fs.existsSync(
        path.join(__dirname, "covers", movie.title_english + ".png")
      )
    ) {
      return;
    } else {
      await download(
        movie.medium_cover_image,
        `covers/${movie.title_english}.png`,
        function () {
          console.log("done");
        }
      );
    }
  });
  return movies;
}

server.listen(port, () => {
  searchAndDownload().then((movies)=>console.log(movies))
});


package.json
{
  "name": "torparty",
  "version": "1.0.0",
  "description": "Group Torrent Streaming",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "nodemon index.js"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/diivi/torparty.git"
  },
  "author": "Divyansh Singh",
  "license": "ISC",
  "bugs": {
    "url": "https://github.com/diivi/torparty/issues"
  },
  "homepage": "https://github.com/diivi/torparty#readme",
  "dependencies": {
    "express": "^4.17.1",
    "request": "^2.88.2",
    "socket.io": "^4.1.3",
    "socket.io-client": "^4.1.3",
    "yts": "^2.0.0"
  }
}
ssomename
  • 505
  • 6
  • 21
  • `download` is async so `allMovies = movies;` is going to execute first. I'd use promises instead of the callback if possible. It's rough going to mix promises and callbacks, but in the interests of getting this working without much fuss, you can add `return new Promise(resolve => download("whatever", "whatever", resolve))`, then change your outer code to `return Promise.all(movies.movies.map((movie) => {...}))` or just `res.send` it directly once it resolves with `then` or `await`. In general, try to simplify your code, get a basic workflow going, then add to it. There's too much at once here. – ggorlen Jul 21 '21 at 21:46
  • Does this answer your question? [How to return the response from an asynchronous call](https://stackoverflow.com/questions/14220321/how-to-return-the-response-from-an-asynchronous-call) – ggorlen Jul 21 '21 at 21:46
  • I will try to rewrite my code, however could you please explain why promises would be more relevant in this case? and also why cant I just somehow send the response after all asynchronous functions have been executed? I will re-read the answer you have linked to too if that can help explain it to me, even though it did'nt make much sense to me at first as promises returning new promises and the hierarchy is a little confusing for me right now but I will try, thanks. – ssomename Jul 21 '21 at 22:48
  • Promises are pretty much categorically better than callbacks -- both achieve the same thing, providing a way to continue execution with new data that arrives asynchronously. If you mix the two, it just turns into a messy situation like this where you have promises that aren't waiting for callbacks to resolve, so you need to promisify the callbacks, at which point you might as well not use CBs entirely unless you have to due to a legacy lib. `Promise.all()` is precisely the tool to wait to send the response until all async funcs have resolved. Yes, promises are confusing for everyone at first. – ggorlen Jul 21 '21 at 22:59
  • I tried to convert the whole code to async/await to organise it as you said, but it still doesn't work the way I want it to, I referred the docs and even asked elsewhere but can't make it work. – ssomename Jul 22 '21 at 13:24
  • I'd love to help, but the code isn't runnable -- I'd suggest stripping out parts that aren't relevant and mocking any calls that are so you can provide a [mcve] that I can run and work with. Otherwise, it's a lot of shooting in the dark that I can't actually validate, or sort of giving hand-wavey examples that you'll still need to do a lot of work to plug into your code. – ggorlen Jul 22 '21 at 16:27
  • Hi, thanks for your patience and help, https://replit.com/join/lsvpvbxkox-divyanshsingh1 this is a minimal reproducible example, you will notice the response data(movies) is logged before "done!" from the download function is logged – ssomename Jul 22 '21 at 16:43
  • also added it to the question in case you don't wanna open repl – ssomename Jul 22 '21 at 16:44
  • Cool, thanks. Would you mind sharing your package.json so I can install the same versions of the dependencies as you? `const cors = require("cors");` and `express` don't really seem necessary here so you could probably skip those. – ggorlen Jul 22 '21 at 16:48
  • yes, I have excluded them from the shorter version – ssomename Jul 22 '21 at 16:50

1 Answers1

1

This code mixes callbacks and promises -- generally not much fun. Promises usually win out because they're categorically better, so fixing the code here boils down to one of two strategies:

  • Convert the callbacks into promises ("promisify") by hand or using a utility (less ideally)
  • Find a better library (or function within the library) that lets you use promises all the way through (ideal).

While you're at it, basically always avoid fs.fooSync methods. This blocks the Node process without much benefit.

Next, since you're using a forEach and promises, you'll need to use a for..of loop and await each item, Promise.all if you want to request everything in parallel, or some sort of task queue if requesting everything in parallel amounts to firing huge number of requests and swamping either you or the remote server. See this answer for more information about these options.

Here's a naive promisification using basically the same setup as you:

const fs = require("fs");
const http = require("http");
const path = require("path");
const request = require("request");
const yts = require("yts");

const download = (uri, filename) => {
  return new Promise((resolve, reject) => {
    request.head(uri, (err, res, body) => {
      if (err) {
        reject(err);
      }

      request(uri)
        .pipe(fs.createWriteStream(filename))
        .on("close", resolve)
      ;
    });
  });
};

const ytsSearch = name => yts.listMovies({limit: 3, query_term: name});

const searchAndDownload = async () => {
  const {movies} = await ytsSearch("minions");
  await Promise.all(movies.map(movie => {
    const filePath = path.join(__dirname, "covers", `${movie.title_english}.png`);

    if (!fs.existsSync(filePath)) {
      download(movie.medium_cover_image, filePath);
    }
  }));
  return movies;
};

searchAndDownload()
  .then(movies => console.log(movies))
;

There's still (at least) two problems. For one, the searchAndDownload function does too many things: fetches and return the movie data, but then also downloads the cover art as a side effect. Functions should do only one thing -- it's better to split this into two different functions, one that fetches the data, and another that downloads and writes cover art to disk.

Secondly, I'd use a modern requests library instead of request such as axios which cuts verbosity and lets you use promises easily.

The side effect of these suggestions is your job will be easier and you'll be less likely to find yourself in some sort of nested callback/promise nightmare.

const axios = require("axios");
const fs = require("fs");
const path = require("path");
const yts = require("yts");

const fileExists = path => fs.promises.stat(path).then(() => true, () => false);

const downloadToFile = async (url, filename) => {
  const response = await axios.get(url, {responseType: "stream"});
  return response.data.pipe(fs.createWriteStream(filename))
};

const downloadMovieCover = async movie => {
  const filePath = path.join(__dirname, "covers", `${movie.title_english}.png`);

  if (!(await fileExists(filePath))) {
    return downloadToFile(movie.medium_cover_image, filePath);
  }
};

const ytsSearch = (term, limit=3) => yts.listMovies({limit, query_term: term});

const searchMovies = async searchTerm => (await ytsSearch(searchTerm)).movies;

(async () => {
  const movies = await searchMovies("minions");
  await Promise.all(movies.map(downloadMovieCover));
  console.log(movies.length);
})()
  .catch(err => console.log(err))
;

Since you're using Express, the IIFE at the bottom with the console.log can be your route handler that responds to the client, with "minions" taken from the request query params.

ggorlen
  • 44,755
  • 7
  • 76
  • 106