1

Before saying to look at the docs, I have and they were not helpful in the slightest.

I have an web page with node as the backbone. On one page I need to request the past 10 images from NASA's Astronomy Picture of the Day (APOD) API and then after that, I need to request the next 5 upcoming launches from the Launch Library API (https://launchlibrary.net/docs/1.3/api.html). My problem is that not all the APODs will load (which I understand because of the nature of asynchronous requests).

Here is my concise app.js file for the Node backend:

app.get("/index", function(req, res) {
    /**Requesting NASA's Astronomy Picture of the Day**/
    var apod_url = "https://api.nasa.gov/planetary/apod?api_key=[My Key]"
    var apod_img_urls = [];
    var curr_moment = moment();
    for(var i = 0; i < 10; i++) {
        var appended_url = apod_url + "&date=" + curr_moment.subtract(i, "days").format("YYYY-MM-DD");
        request(appended_url, function(error, reponse, body) {
            if(!error && reponse.statusCode == 200) {
                var img_json = JSON.parse(body);
                if(img_json.media_type == "image") {
                    var apod_promise = new Promise(function(resolve, reject){
                        resolve(img_json.hdurl);
                    });
                    apod_img_urls.push(apod_promise);
                }
            } else {
                console.log(error);
            }
        });
    }
    /**************************************************/

    var url = "https://launchlibrary.net/1.3/launch?next=20&mode=verbose";
    request(url, function(error, response, body) {
       if(!error && response.statusCode == 200) {
           var data = JSON.parse(body);
           res.render("index", {data: data, apod_img_urls: apod_img_urls});
       } else {
           console.log(error);
       }
    });
});

Here is an EJS snippet

<% apod_img_urls.forEach(function(promise, index) { %>
    <div class="carousel-item <%= (index == 0 ? 'active' : '') %>">
        <div class="w-100 home-image" style="background-image: url('<%= promise.then(function(url) {return url}); %>')"></div>
    </div>
<% }); %>

When I check in the source it shows that the background image urls for the divs are [object Promise]. The way I have it, no images show up. Also the number of divs displayed (i.e. the number of images I should have) is variable; sometimes it's 5, sometimes it's 3, and other times it's none. Could my problem be that I'm rendering the page inside of another request? Also how can I get the actual image URL to show up in the EJS file?

georg
  • 211,518
  • 52
  • 313
  • 390
Christopher Berry
  • 685
  • 2
  • 7
  • 18

2 Answers2

2

You are creating the promise too late, inside the asynchronous callback to reequest - fairly simple reorganisation of code required

Once all the promises are in an array, you then need to wait for them to complete, using Promise.all

app.get("/index", function(req, res) {
    /**Requesting NASA's Astronomy Picture of the Day**/
    var apod_url = "https://api.nasa.gov/planetary/apod?api_key=[My Key]"
    var promises = [];
    var curr_moment = moment();
    for(var i = 0; i < 10; i++) {
        var appended_url = apod_url + "&date=" + curr_moment.subtract(i, "days").format("YYYY-MM-DD");
        promises.push(new Promise((resolve, reject) => {
            request(appended_url, function(error, reponse, body) {
                if(!error && reponse.statusCode == 200) {
                    var img_json = JSON.parse(body);
                    resolve(img_json.hdurl);
                } else {
                    reject(error);
                    console.log(error);
                }
            });
        }));
    }
    Promise.all(promises).then(apod_img_urls => {
        var url = "https://launchlibrary.net/1.3/launch?next=20&mode=verbose";
        request(url, function(error, response, body) {
           if(!error && response.statusCode == 200) {
               var data = JSON.parse(body);
               res.render("index", {data, apod_img_urls});
           } else {
               console.log(error);
           }
        });
    });
});

Rather than using request-promise and all the cruft it requires, you can always make your own promisified request function, and in this case, it rejects if status is anything other than 200

const requestP = url => new Promise((resolve, reject) => request(url, (error, response, body) => {
    if (error) {
        reject({error, response, body});
    } else if (resonse.statusCode != 200) {
        reject(resonse.statusCode);
    } else {
        resolve({response, body});
    }
}));

now, your code can be written as:

app.get("/index", function(req, res) {
    /**Requesting NASA's Astronomy Picture of the Day**/
    const apod_url = "https://api.nasa.gov/planetary/apod?api_key=[My Key]"
    const curr_moment = moment();
    Promise.all(Array.from({length:10}, (_, i) => {
        const appended_url = apod_url + "&date=" + curr_moment.subtract(i, "days").format("YYYY-MM-DD");
        return requestP(appended_url).then(({response, body}) => JSON.parse(body).hdurl);
    })).then(apod_img_urls => {
        const url = "https://launchlibrary.net/1.3/launch?next=20&mode=verbose";
        return requestP(url).then(({response, body}) => {
            const data = JSON.parse(body);
            return res.render("index", {data, apod_img_urls});
        });
    });
});   

Note: there's a lot of ES2015+ going on in there

Jaromanda X
  • 53,868
  • 5
  • 73
  • 87
  • Thank you this worked. I knew it had to be an organization thing. There is one problem with the apod_img_urls; for some reason the final url is undefined. Is there a common solution to this? EDIT: Turns out the url is actually undefined so I'm guessing it's a problem with the API> – Christopher Berry Feb 07 '18 at 22:34
1

If you use a promise-returning request library like request-promise then you can do something like:

app.get("/index", function(req, res) {
    var apod_url = "https://api.nasa.gov/planetary/apod?api_key=[My Key]"
    var curr_moment = moment();
    var urls = [];
    for(var i = 0; i < 10; i++) {
        urls[i] = apod_url + "&date=" + curr_moment.subtract(i, "days").format("YYYY-MM-DD");
    }
    Promise.all(urls.map(url => rp({ url, json: true})).then((results) => {
      // here you have all results with JSON already parsed for you
      // ...
    }).catch((err) => {
      // handle error
      // make sure to return response to client
      // ...
    });
    // ...
});

If you want to work with promises then use a promise-returning module like request-promise or axios instead of the standard request, see:

Or, alternatively, use bluebird.promisify or built-in util.primisify (since 8.0) to promisify the request module, see:

For more options of promisifying request see:

Then, when you have a promise-returning request library, make an array of URLs however you want, then map that array with the request functions, like this:

let rp = require('request-promise');
let urls = ['http://...', 'http://...'];
let promises = urls.map(rp);

and then use Promise.all to wait for all of them to finish, while being done concurrently:

Promise.all(promises)
  .then(...)
  .catch(...);

or, if you're using async/await, then:

try {
  let x = await Promise.all(promises);
  ...
} catch (err) {
  ...
}

Many of modules like axios or request-promise-json or request-promise will parse JSON for you if you run it correctly, see:

Avoid parsing JSON yourself but if you do then always put JSON.parse() inside of try/catch (or use my little tryjson module) - see those answers to know why:

rsp
  • 107,747
  • 29
  • 201
  • 177