Search code examples
node.jsaws-lambdaalexa-skills-kitalexa-slot

How to make an asynchronous api call for Alexa Skill application with a Lambda function?


I want to call an api from a Lambda function. My handler is triggered by an intent which includes two required slots. Therefore I don't know in advance whether I will be returning a Dialog.Delegate directive or my response from the api request. How do I promise these return values by the time the intent handler is called?

This is my handler:

const FlightDelayIntentHandler = {
  canHandle(handlerInput) {
    return handlerInput.requestEnvelope.request.type === 'IntentRequest'
      && handlerInput.requestEnvelope.request.intent.name === 'MyIntent';
  },

  handle(handlerInput) {
    const request = handlerInput.requestEnvelope.request;

    if (request.dialogState != "COMPLETED"){
      return handlerInput.responseBuilder
        .addDelegateDirective(request.intent)
        .getResponse();
    } else {
      // Make asynchronous api call, wait for the response and return.
      var query = 'someTestStringFromASlot';

      httpGet(query,  (result) => {
        return handlerInput.responseBuilder
          .speak('I found something' + result)
          .reprompt('test')
          .withSimpleCard('Hello World', 'test')
          .getResponse();
      });
    }
  },
};

This is my helper function which makes the request:

const https = require('https');

function httpGet(query, callback) {
    var options = {
        host: 'myHost',
        path: 'someTestPath/' + query,
        method: 'GET',
        headers: {
            'theId': 'myId'
        }
    };

    var req = https.request(options, res => {
        res.setEncoding('utf8');
        var responseString = "";

        //accept incoming data asynchronously
        res.on('data', chunk => {
            responseString = responseString + chunk;
        });

        //return the data when streaming is complete
        res.on('end', () => {
            console.log('==> Answering: ');
            callback(responseString);
        });

    });
    req.end();
}

So I suspect I will have to use promises and put an "async" in front of my handle function? I am very new to all of this so I don't know the implications of this, especially considering the fact that I have two different return values, one directly and the other one delayed. How would I solve this?

Thank you in advance.


Solution

  • As you suspected, your handler code is finishing before the asynchronous call to http.request, hence the Alexa SDK receives no return value from the handle function and will return an invalid response to Alexa.

    I slightly modified your code to run it locally on a laptop to illustrate the issue :

    const https = require('https');
    
    function httpGet(query, callback) {
        var options = {
            host: 'httpbin.org',
            path: 'anything/' + query,
            method: 'GET',
            headers: {
                'theId': 'myId'
            }
        };
    
        var req = https.request(options, res => {
            res.setEncoding('utf8');
            var responseString = "";
    
            //accept incoming data asynchronously
            res.on('data', chunk => {
                responseString = responseString + chunk;
            });
    
            //return the data when streaming is complete
            res.on('end', () => {
                console.log('==> Answering: ');
                callback(responseString);
            });
    
        });
        req.end();
    }
    
    function FlightDelayIntentHandler() {
    
        // canHandle(handlerInput) {
        //   return handlerInput.requestEnvelope.request.type === 'IntentRequest'
        //     && handlerInput.requestEnvelope.request.intent.name === 'MyIntent';
        // },
    
        // handle(handlerInput) {
        //   const request = handlerInput.requestEnvelope.request;
    
        // if (request.dialogState != "COMPLETED"){
        //     return handlerInput.responseBuilder
        //       .addDelegateDirective(request.intent)
        //       .getResponse();
        //   } else {
            // Make asynchronous api call, wait for the response and return.
            var query = 'someTestStringFromASlot';
    
            httpGet(query,  (result) => {
                console.log("I found something " + result);
    
            //   return handlerInput.responseBuilder
            //     .speak('I found something' + result)
            //     .reprompt('test')
            //     .withSimpleCard('Hello World', 'test')
            //     .getResponse();
            });
    
            console.log("end of function reached before httpGet will return");
        //   }
        // }
    }
    
    FlightDelayIntentHandler();
    

    To run this code, do not forget npm install http , then node test.js. It produces

    stormacq:~/Desktop/temp $ node test.js
    end of function reached before httpGet will return
    ==> Answering:
    I found something {
      "args": {},
      "data": "",
    ... 
    

    So, the key is to wait for http get to return before to return a response to Alexa. For that, I propose to modify your httpGet function to return a promise instead using callbacks.

    Modified code is like this (I kept your original code as comment)

    const https = require('https');
    
    async function httpGet(query) {
        return new Promise( (resolve, reject) => {
            var options = {
                host: 'httpbin.org',
                path: 'anything/' + query,
                method: 'GET',
                headers: {
                    'theId': 'myId'
                }
            };
    
            var req = https.request(options, res => {
                res.setEncoding('utf8');
                var responseString = "";
    
                //accept incoming data asynchronously
                res.on('data', chunk => {
                    responseString = responseString + chunk;
                });
    
                //return the data when streaming is complete
                res.on('end', () => {
                    console.log('==> Answering: ');
                    resolve(responseString);
                });
    
                //should handle errors as well and call reject()!
            });
            req.end();
    
        });
    
    }
    
    
    
    async function FlightDelayIntentHandler() {
    
            // canHandle(handlerInput) {
        //   return handlerInput.requestEnvelope.request.type === 'IntentRequest'
        //     && handlerInput.requestEnvelope.request.intent.name === 'MyIntent';
        // },
    
        // handle(handlerInput) {
        //   const request = handlerInput.requestEnvelope.request;
    
        // if (request.dialogState != "COMPLETED"){
        //     return handlerInput.responseBuilder
        //       .addDelegateDirective(request.intent)
        //       .getResponse();
        //   } else {
            // Make asynchronous api call, wait for the response and return.
            var query = 'someTestStringFromASlot';
    
            var result = await httpGet(query);
            console.log("I found something " + result);
    
            //   return handlerInput.responseBuilder
            //     .speak('I found something' + result)
            //     .reprompt('test')
            //     .withSimpleCard('Hello World', 'test')
            //     .getResponse();
            //});
    
            console.log("end of function reached AFTER httpGet will return");
        //   }
        // }
    }
    
    FlightDelayIntentHandler();
    

    Running this code produces :

    stormacq:~/Desktop/temp $ node test.js
    ==> Answering:
    I found something{
      "args": {},
      "data": "",
    ...
    end of function reached AFTER httpGet will return