0

I recently started refactoring older callback-hell routes to use promises. (Note: the refactor is not complete so some of the following code is ugly)

Doing so broke my res.render() function.

  • I am using EJS for my render engine.
  • Most of my promise chains are built off of MongoDB / Mongoose queries.

The basic app structure is a complex survey / quiz that sorts users into different categories. It stores their info and renders a custom EJS page at the end. Here's how the final call works, basically:

         mongoose.model('Result')
             .findOne({quizId: req.query.id})
             .then(getPageData)
             .then(renderPage)
             .catch(errorHandler)

Simple enough, in theory.

Relevant functions:

    let getPageData = (mongooseResult) => {

      let resultsPage;
      let analyticsData;

      if (mongooseResult.answers.section1.question1.answer != null && mongooseResult.answers.section1.question1.answer != undefined){
          let resultsPage = WP.getPage(wp_pages_data, 'slug', mongooseResult.coreFit);
          let analyticsData = {
            quizId: mongooseResult.quizId,
            coreFit: mongooseResult.coreFit,
            secondFit: mongooseResult.secondFit,
            degree: mongooseResult.answers.section1.question1.answer,
            year: mongooseResult.answers.section1.question2.answer,
            concentration: mongooseResult.answers.section1.question3.answer,
            views: mongooseResult.views,
          }

        mongooseResult.views++;
        mongooseResult.save();

        return [resultsPage, analyticsData, mongooseResult];
      } else {
        throw new Error('S1Q1 IS UNDEFINED');
      }
    };
    let renderPage = ([resultsPage, analyticsData, mongooseResult]) => {
      if (resultsPage != null && resultsPage != undefined){

        // WHY IS MY RENDER NOT WORKING???
        res.render('templates/' + mongooseResult.coreFit,  Object.assign({}, WP.get_WP_data(resultsPage), getPartials(mongooseResult.modules), analyticsData), (err, html) => {if (err) console.log(err); else res.send(html);});
      } else {
        throw new Error('GETTING PAGE DATA RETURNED UNDEFINED');
      }
    };

Incoming requests are first routed to a /submit POST route - which manipulates some data, stores it all in the DB, then does res.redirect() to the /results?id=12345 GET route, which runs the logic above.

What's funky is that the render() actually fires; the callback ( (err, html) => {if (err) console.log(err); else res.send(html);} ) sends rendered HTML to IF YOU HIT THE ROUTE A SECOND TIME.

Completing the survey does NOT generate a response; but it DOES successfully store all our info in the DB and console.logs the ID to which the client request should redirect. Hitting the GET route with this generated ID / URL, literally by copy-pasting it, renders just fine.

Swapping the render for a simple res.send('wtf') works - an actual response is sent to the client. (in this case the string 'wtf' ... )

The response just doesn't send back data. I don't get it. Any ideas?

UPDATE: Strangely enough, hitting the route manually vs the redirect from my earlier POST request produce different results with a simple res.send('wtf mate').

Hitting the GET route and passing in a previously existing ID to the req.query manually:enter image description here

Hitting the GET route AFTER the res.redirect() call from the prior POST request:enter image description here

This might be more an Express request type mismatch issue? Why would a res.redirect('/results?id=someString') call mutate the URL vs sending back a string / HTML?

Zfalen
  • 878
  • 6
  • 20
  • why You pass anonymous function as last argument? `res.render` and sends it to client. I don't see necessity to do additional `res.send`. – num8er Jun 05 '17 at 17:35
  • You're right - In this case for debugging only; I wanted to see whether adding the extra callback explicitly did anything to help - or if there was a secret `err` being swallowed behind the scenes. – Zfalen Jun 05 '17 at 17:43
  • Since you're using code like `mongooseResult.save()`, are you sure that the `POST /submit` actually _waits_ for the database saves/updates to be completed before it redirects? – robertklep Jun 05 '17 at 18:52

2 Answers2

2

Based on your update, it sounds like your res.send() call is being interpreted strangely on the client side. POST request should not affect the URL bar unless you've specifically told it to - have you checked where the client initially sends this request?

My guess - since you say that the routing code you posted above is being hit from res.redirect() on a prior POST route - is that there is client-side logic expecting a response object to be a URL string, and hitting the GET route for the first time via your redirect carries over this response object. So that the client is attempting to insert a massive string of HTML into the URL bar.

In this case, hitting the GET route manually, like you said, would simply render the HTML from res.render() by default - but your initial hit from the res.redirect() might not.

  • DOH! You nailed it.... Here is part of my client-side AJAX: `success: (res) => { window.location = res; }` So, yeah, you're right. Since `res.render()` returns an HTML string, the URL bar didn't have a clue what to do with it. But the default GET behavior expects HTML - so manually pinging the route behaved correctly. Instead of doing `res.redirect()`, I just did a `res.send('results?id=12345')` - which redirects the client to my GET route and it all is well with the world! Thanks! – Zfalen Jun 05 '17 at 21:08
1

Let's try to debug issue by testing code parts.

I've few clues, one of them is that 'templates/' + result.coreFit does not have template.
Try to check coreFit value if there is a file in templates folder with same value-name.

Try this code and tell me if it's working:

const 
  _ = require('lodash'),
  mongoose = require('mongoose');

const getQuizResultById = quizId => {
    return new Promise((resolve, reject) => {
        const 
            Result = mongoose.model('Result'),
            query = {
                quizId
            };

        Result
            .findOne(query)
            .then(resolve)
            .catch(reject);
    });
};

const getPageData = result => {
    if (_.get(result, 'answers.section1.question1.answer')) {
        let resultsPage = WP.getPage(wp_pages_data, 'slug', result.coreFit);
        let 
        analyticsData = _.pick(result, ['quizId', 'coreFit', 'secondFit', 'views']);
        analyticsData.degree = _.get(result, 'answers.section1.question1.answer');
        analyticsData.year = _.get(result, 'answers.section1.question2.answer');        
        analyticsData.concentration = _.get(result, 'answers.section1.question3.answer');

        result.views++;
        result.save();

        return [resultsPage, analyticsData, result];
    }

    throw new Error('S1Q1 IS UNDEFINED');
}

const renderPage = ([resultsPage, analyticsData, result], res) => {
    if (resultsPage) {
        return res.render(
            'templates/' + result.coreFit,  
            Object.assign({}, WP.get_WP_data(resultsPage), getPartials(result.modules), analyticsData), 
            (err, html) => {
                if (err) {
                    console.log(err);
                    return res.status(500).send('Ooopsss...');
                }
                res.send(html);
            });
    } 

    throw new Error('GETTING PAGE DATA RETURNED UNDEFINED');
};

router.get('/results', (req, res) => {
    getQuizResultById(req.query.id)
        .then(getPageData)
        .then(data => {
           renderPage(data, res);
        })
        .catch(errorHandler);
});
num8er
  • 18,604
  • 3
  • 43
  • 57
  • 1
    Thanks for the response, @num8er ! Turns out it was a terrible client-side oversight on my part during the refactor process. Check out Lars' answer above - the critical clue was how my URL bar re-routed when hitting the GET route as part of a redirect when submitting the final survey.... Totally spaced this piece since 99% of the refactor is server-side – Zfalen Jun 05 '17 at 21:11