Dustin Coates in Alexa

ValidateRequest: A continued in-depth analysis of the Alexa Skills Kit SDK for Node.js

In the last post looking at the HandleLambdaEvent, we left where today’s function was to be called. As a reminder, this is the Dig Deep series, where we look line-by-line at the tools and libraries we use to build voice-first experiences. This is not the place to go for tutorials, but if you want to learn interesting little nuggets about what you use every day, off we go…

ValidateRequest is one of the larger functions in this file, with a number of conditionals, but it’s quite simple when your break it down into its component parts.

ValidateRequest does 5 things:

  • Grabs the applicationId and userId of the current request
  • Checks that the request was authorized if an authorized application ID has been set
  • Grabs the session data from DynamoDB if a table name was provided
  • Handles errors
  • Calls EmitEvent

Here is the code in full. Click here to skip to the examinations.:

function ValidateRequest() {
    var event = this._event;
    var context = this._context;
    var callback = this._callback;
    var handlerAppId = this.appId;

    var requestAppId = '';
    var userId = '';

    // Long-form audio enabled skills use event.context
    if (event.context) {
        requestAppId = event.context.System.application.applicationId;
        userId = event.context.System.user.userId;
    } else if (event.session) {
        requestAppId = event.session.application.applicationId;
        userId = event.session.user.userId;
    }


    if(!handlerAppId){
        console.log('Warning: Application ID is not set');
    }

    try {
        // Validate that this request originated from authorized source.
        if (handlerAppId && (requestAppId !== handlerAppId)) {
            console.log(`The applicationIds don\'t match: ${requestAppId} and ${handlerAppId}`);
            var error = new Error('Invalid ApplicationId: ' + handlerAppId)
            if(typeof callback === 'undefined') {
                return context.fail(error)
            } else {
                return callback(error);
            }
        }

        if(this.dynamoDBTableName && (!event.session.sessionId || event.session['new']) ) {
            attributesHelper.get(this.dynamoDBTableName, userId, (err, data) => {
                if(err) {
                    var error = new Error('Error fetching user state: ' + err)
                    if(typeof callback === 'undefined') {
                        return context.fail(error)
                    } else {
                        return  callback(error);
                    }
                }

                Object.assign(this._event.session.attributes, data);

                EmitEvent.call(this);
            });
        } else {
            EmitEvent.call(this);
        }
    } catch (e) {
        console.log(`Unexpected exception '${e}':\n${e.stack}`);
        if(typeof callback === 'undefined') {
            return context.fail(e)
        } else {
            return  callback(e);
        }
    }
}

Variable Setting

Our first seven lines see a setting of variable values for event, context, callback, and handlerAppId, all off the current context, and requestAppId and userId as empty strings. Nothing special here, though useful to know what we’re working with.

Long-form Audio Context

if (event.context) {
    requestAppId = event.context.System.application.applicationId;
    userId = event.context.System.user.userId;
} else if (event.session) {
    requestAppId = event.session.application.applicationId;
    userId = event.session.user.userId;
}

This is a nod to the fact that long-form audio does not generally have sessions—the user starts a song and then can come back to it (to, for example, pause or fast-forward) and so any application ID or user ID information we need to get would be on the context, rather than the session.

The interesting thing here is that we always provide preference to the context. Best I can tell, there aren’t any events where there will be session without context, but do tweet at me if I’m missing something.

Matching Application IDs

Next up we’ve got a simple console.log statement warning us if we haven’t set an application ID. (Seriously, just do it, it takes five seconds. Unless you’re overloading this by using the same Lambda for multiple applications which isn’t advisable.)

I’m not reproducing the next part so as to not gum up this post with a ton of duplicate code, but the general flow is:

  • Wrap inside a try/catch block.
  • If there’s an application ID set and it’s not the same as the requesting ID, log a message to the console and respond with an error.

There’s really just one interesting part that deserves calling out. What happens when the callback is undefined? Well, that’s sort of a weird question, because the callback is coming from the Alexa service and so should never be undefined.

Well, in the oldest runtime of Node.js on AWS Lambda, there was no callback as a third argument to the Lambda function. Inside, the context object would provide methods—one of which is context.fail, which is called here in place of the callback. This version of Node.js won’t be supported for much longer, so you can expect this to be cleaned up and removed sooner or later.

(We also see this same pattern repeated in the try block and while getting the user’s state from DynamoDB.)

Get the Current State from DynamoDB

Next up we’ve got a conditional that checks to see if we’ve set our DynamoDB table name and we’re at the beginning of a session (which we know because there’s either no session ID or new is true). If those conditions aren’t met, we just emit the event.

If they are, then we’re using the DynamoDB attributes helper (which we’ll look at in another post) to get the current user’s state from the specified table and assign it to the attributes object on session. Again—remember that this only works if we give our function DynamoDB execution permissions. Then we emit the event.

In the next post, we’ll leave this file and head over to the DynamoDB attributes helper to get a clearer look at what we just saw. Until then…