How to retrieve metadata from Stripe session object NodeJS - node.js

I am trying to pass a UID and purchase ID with Stripe Checkout session object (using metadata). Generating the session ID on my server attaching the metadata works very fine. Stripe also POSTs everything correctly to my webhook server. The problems occurs while retrieving the metadata from the session object POSTed by Stripe.
Here is the error I get
TypeError: Cannot read property 'metadata' of undefined at /app/app.js:35:32
Here is the session obj posted by Stripe-
{
"id": "evt_1GRC7lAfcfWZXl7jQ3VzNo4y",
"object": "event",
"api_version": "2019-10-17",
"created": 1585292221,
"data": {
"object": {
"id": "cs_test_gLsHqtF8XhB3C3DlWKcLtNdTitp0St8ju5qgJgl6tHrMxxWvju9gb9Li",
"object": "checkout.session",
"billing_address_collection": null,
"cancel_url": "https://andropaym.firebaseapp.com/fail.html",
"client_reference_id": null,
"customer": "cus_GzASi1Klpydh8x",
"customer_email": null,
"display_items": [
{
"amount": 37500,
"currency": "inr",
"custom": {
"description": "Carefully modified Linux Distro Bundle for Android.",
"images": null,
"name": "Modded OS Bundle"
},
"quantity": 1,
"type": "custom"
}
],
"livemode": false,
"locale": null,
"metadata": {
"uid": "EB1m6nAOTVNcQhHO2O7COspap8y1",
"payID": "GPA.5620-9852-7063-44324"
},
"mode": "payment",
"payment_intent": "pi_1GRC7EAfcfWZXl7jhixrWHRS",
"payment_method_types": [
"card"
],
"setup_intent": null,
"shipping": null,
"shipping_address_collection": null,
"submit_type": null,
"subscription": null,
"success_url": "https://andropaym.firebaseapp.com/success.html"
}
},
"livemode": false,
"pending_webhooks": 4,
"request": {
"id": null,
"idempotency_key": null
},
"type": "checkout.session.completed"
}
Here is my webhook code -
const express = require('express');
const app = express();
const bodyParser = require('body-parser');
const stripe = require('stripe')('sk_test_xxxx');
const endpointSecret = 'whsec_xxxx';
// set the port of our application
// process.env.PORT lets the port be set by Heroku
var port = process.env.PORT || 8080;
app.post('/', bodyParser.raw({type: 'application/json'}), (request, response) => {
const sig = request.headers['stripe-signature'];
let event;
try {
event = stripe.webhooks.constructEvent(request.body, sig, endpointSecret);
} catch (err) {
return response.status(400).send(`Webhook Error: ${err.message}`);
}
function handleCheckoutSession(uid) {
// Here we are getting the session obj and we can process it to check for the things we need
console.log("UID is " + uid);
}
// Handle the checkout.session.completed event
if (event.type === 'checkout.session.completed') {
const session = event.data.object;
let uid = request.data.metadata.uid;
// Fulfill the purchase...
handleCheckoutSession(uid);
}
// Return a response to acknowledge receipt of the event
response.json({received: true});
});
app.listen(port, function () {
console.log('Our app is running on http://localhost:' + port);
});
module.exports = app;
The code works fine without the metadata being parsed
More code links:
1. Highlighted error webhook code - https://gist.github.com/imprakharshukla/1e2315615983e0e9d492d2288e159832#file-webhook_backend-js-L40

You need to use the object returned by stripe.constructEvent, not the request body.
Change
let uid = request.data.metadata.uid;
to
let uid = session.metadata.uid
and it should work as expected.

Related

Firebase Cloud function request.body empty or undefined

const functions = require("firebase-functions");
const Razorpay = require("razorpay");
const cors = require("cors")({origin: true});
exports.razorpayverification = functions.https.onRequest((req, res) => {
try {
const name = JSON.stringify(req.body);
console.log(req.body, name);
res.status(200).json({status: "ok"});
// I need req.body.amount value. req.body is empty. but JSON.stringify(req.body) contains all the data.
} catch (error) {
res.status(400).json({error: "JSON error"});
res.end();
}
});
I am using the above cloud function and receiving payment data from Razorpay webhooks. In my console.log I have called req.body and name (const name = JSON.stringify(req.body);). name returns the data in string format and contains all information like orderid, payment id, amount etc but req.body is empty or undefined. I tried enclosing the above within cors but still the same result. Kindly let me know how to read req.body. I need to read req.body.amount.
Output:
req.body:
{
name: {"entity":"event","account_id":"acc_IvsP3zm1EoP7me","event":"payment.captured","contains":["payment"],"payload":{"payment":{"entity":{"id":"pay_J9fXFPd7T9uWo5","entity":"payment","amount":1000,"currency":"INR","status":"captured","order_id":"order_J9fWySTzmrenBl","invoice_id":null,"international":false,"method":"card","amount_refunded":0,"refund_status":null,"captured":true,"description":"Recharge","card_id":"card_J9fXFSITSAyVdB","card":{"id":"card_J9fXFSITSAyVdB","entity":"card","name":"k","last4":"1111","network":"Visa","type":"debit","issuer":null,"international":false,"emi":false,"sub_type":"consumer","token_iin":null},"bank":null,"wallet":null,"vpa":null,"email":"test#test.com","contact":"+16505551234","notes":{"key1":"otzo","key2":"recharge","address":"Razorpay Corporate Office"},"fee":20,"tax":0,"error_code":null,"error_description":null,"error_source":null,"error_step":null,"error_reason":null,"acquirer_data":{"auth_code":"213319"},"created_at":1647850380}}},"created_at":1647850385}
As shown req.body returns only the open curly braces and nothing after that. name returns all the data.
Not sure this is the correct answer but my code was executing properly and I was receiving the res.body. But issue lies in the console.log which did not display. if it is a multi-level object firebase cloud function is not displaying properly. Below is the Razorpays Payments Sample Payload. res.body displays only open end curly bracket. res.body.payload created log with all the details under the payload section. Again here firebase cloud function displays each and every field as a separate log.
{ "entity": "event", "account_id": "acc_BFQ7uQEaa7j2z7", "event": "payment.authorized", "contains": [ "payment" ], "payload": { "payment": { "entity": { "id": "pay_DESlfW9H8K9uqM", "entity": "payment", "amount": 100, "currency": "INR", "status": "authorized", "order_id": "order_DESlLckIVRkHWj", "invoice_id": null, "international": false, "method": "netbanking", "amount_refunded": 0, "refund_status": null, "captured": false, "description": null, "card_id": null, "bank": "HDFC", "wallet": null, "vpa": null, "email": "gaurav.kumar#example.com", "contact": "+919876543210", "notes": [], "fee": null, "tax": null, "error_code": null, "error_description": null, "error_source": null, "error_step": null, "error_reason": null, "acquirer_data": { "bank_transaction_id": "0125836177" }, "created_at": 1567674599 } } }, "created_at": 1567674606}

How to destructure a Stripe object

I have written the following web hook to capture events coming from stripe after a user has amended a subscription:
export const stripeWebhookEvents = functions.https.onRequest( async ( request: any, response: any ) => {
// Retrieve the event by verifying the signature using the raw body and secret.
let event;
const signature = request.headers["stripe-signature"];
try {
event = stripe.webhooks.constructEvent(
request.rawBody,
signature,
STRIPE_WEBHOOK
);
} catch (err) {
console.log("⚠️ Webhook signature verification failed.", err);
return response.sendStatus(400);
}
// Extract the object from the event.
const eventType = event.type;
console.log(`Scripe event type: ${eventType}.`);
// Handle the event
switch (eventType) {
case "customer.subscription.created": {
// Occurs whenever a customer is signed up for a new plan.
console.log("customer.subscription.created");
try {
const session = event.data.object;
console.log("stripe customer: ", session.customer);
const userRecord = await getUserWithStripeCustomerID(session.customer);
console.log("userRecord: ", userRecord);
const data = {
"subscription_status": session.status,
};
await updateUser(userRecord, data);
} catch (error) {
console.log(error);
}
break;
}
case "customer.subscription.updated": {
// Occurs whenever a subscription changes (e.g., switching from one plan to another,
// or changing the status from trial to active).
break;
}
case "customer.subscription.deleted": {
// The subscription has been deleted so update the user document so that it shows
// that the subscription is inactive.
}
case "invoice.payment_failed": {
// The payment failed or the customer does not have a valid payment method.
// The subscription becomes past_due. Notify your customer and send them to the
// customer portal to update their payment information.
// update the user document so that it shows that the subscription is inactive.
break;
}
default: {
// Unexpected event type
console.log(`Unhandled event type ${eventType}.`);
}
}
response.sendStatus(200);
});
The subscription object that is received from stripe is:
{
"id": "sub_JhxDD458se1T1c",
"object": "subscription",
"application_fee_percent": null,
"automatic_tax": {
"enabled": false
},
"billing_cycle_anchor": 1624221102,
"billing_thresholds": null,
"cancel_at": null,
"cancel_at_period_end": false,
"canceled_at": null,
"collection_method": "charge_automatically",
"created": 1624221102,
"current_period_end": 1626813102,
"current_period_start": 1624221102,
"customer": "cus_FJ9HCZk3rCSlxD",
"days_until_due": null,
"default_payment_method": null,
"default_source": null,
"default_tax_rates": [],
"discount": null,
"ended_at": null,
"items": {
"object": "list",
"data": [
{
"id": "si_JhxDPLJExCLmnu",
"object": "subscription_item",
"billing_thresholds": null,
"created": 1624221103,
"metadata": {},
"price": {
"id": "price_1J3q2zHx6drz9dmy2bMdkXOs",
"object": "price",
"active": true,
"billing_scheme": "per_unit",
"created": 1624054821,
"currency": "gbp",
"livemode": false,
"lookup_key": null,
"metadata": {},
"nickname": null,
"product": "prod_JhEWBSIjTi2nlt",
"recurring": {
"aggregate_usage": null,
"interval": "month",
"interval_count": 1,
"usage_type": "licensed"
},
"tax_behavior": "unspecified",
"tiers_mode": null,
"transform_quantity": null,
"type": "recurring",
"unit_amount": 1299,
"unit_amount_decimal": "1299"
},
"quantity": 1,
"subscription": "sub_JhxDD458se1T1c",
"tax_rates": []
}
],
"has_more": false,
"url": "/v1/subscription_items?subscription=sub_JhxDD458se1T1c"
},
"latest_invoice": null,
"livemode": false,
"metadata": {},
"next_pending_invoice_item_invoice": null,
"pause_collection": null,
"pending_invoice_item_interval": null,
"pending_setup_intent": null,
"pending_update": null,
"schedule": null,
"start_date": 1624221102,
"status": "active",
"transfer_data": null,
"trial_end": null,
"trial_start": null
}
When the "customer.subscription.created" I want to update the price id ("price_1J3q2zHx6drz9dmy2bMdkXOs") but I am not sure how to retrieve it. I have tried:
session.items.data[price.id]
but it doesn't work. How do I access the price id?

Mongodb Schema issue

Here i am using Nodejs application to get JSON response from external API.I need to capture few key-value pair of this response and need to save it MongoDB.
I am getting the response properly, but i am unable to save the data in database.
Requirement:
Each time i get this response from External server , i need to save it in table by rewriting any documents if already exists in this collection. Here i have nearly 7 array items in json response , i need to save corresponding key value pair from all of the items automatically .
Model:
var mongoose = require('mongoose');
const getAllUsersDataSchema = new mongoose.Schema({
userRefID:[] ,
userName:[],
divisionId: [{}],
divisionName:[{}],
emailId :[{}],
})
module.exports = getAllUsers = mongoose.model('getAllUsers',getAllUsersDataSchema );
**API Call where i am capturing external API response:**
const express = require('express');
const router = express.Router();
const request = require('request');
const config = require('../config');
const fs = require('fs');
const getAllUsers = require ('../db/getAllUsersListmodel');
var mongoose = require('mongoose');
mongoose.connect ('mongodb://localhost/testdb',{ useUnifiedTopology: true , useNewUrlParser: true });
router.get('/', (req, res) => {
// token in session -> get user data and send it back to the Angular app
var data =fs.readFileSync('../teamlist.txt', {encoding:'utf8'} )
console.log(data);
if (data) {
request(
{
method: 'GET',
url: 'https://api.mypurecloud.com/api/v2/users',
headers: {
'Authorization': 'Bearer ' + data
}
},
// callback
(error, response, body) => {
let userInfoResponse = JSON.parse(body);
res.send(userInfoResponse);
console.log(userInfoResponse.entities.length)
console.log(userInfoResponse.entities[0].division.id)
getAllUsers.create({
userRefID : userInfoResponse.entities.id,
userName: userInfoResponse.entities.name,
divisionId: userInfoResponse.entities.division.id,
divisionName:userInfoResponse.entities.division.name,
emailId:userInfoResponse.entities.primaryContactInfo.address
}, (error,post)=>{
console.log(error,post);
});
}
);
}
// no token -> send nothing
else {
res.send("Token Not Present - Kindly login in back");
}
//console.log(req.session.token);
});
Data is saving in DB but not getting any array data saved in to it.
{
"_id" : ObjectId("5fd998d61439a434983702cd"),
"userRefID" : [ ],
"userName" : [ ],
"__v" : 0
}
This is exact API JSON response i am trying to save it in DB and use it for future references:
{
"entities": [
{
"id": "07f426ff-506f-4e5e-afdb-2c7397edac61",
"name": "EPS Purecloud Support",
"division": {
"id": "36852a81-ad7f-4c71-a1cd-7f431c05179f",
"name": "",
"selfUri": "/api/v2/authorization/divisions/36852a81-ad7f-4c71-a1cd-7f431c05179f"
},
"chat": {
"jabberId": "5dcc25e1db8c7e19238a287d#cognizant3.orgspan.com"
},
"email": "eps#genesys.com",
"primaryContactInfo": [
{
"address": "eps#genesys.com",
"mediaType": "EMAIL",
"type": "PRIMARY"
}
],
"addresses": [],
"state": "active",
"username": "eps#genesys.com",
"version": 3,
"acdAutoAnswer": false,
"selfUri": "/api/v2/users/07f426ff-506f-4e5e-afdb-2c7397edac61"
},
{
"id": "c5ce06dc-6265-4d16-be18-f5fc5a918295",
"name": "Generic",
"division": {
"id": "36852a81-ad7f-4c71-a1cd-7f431c05179f",
"name": "",
"selfUri": "/api/v2/authorization/divisions/36852a81-ad7f-4c71-a1cd-7f431c05179f"
},
"chat": {
"jabberId": "5ebab3dba6686314f6913b98#cognizant3.orgspan.com"
},
"email": "integration-generic-a03293c0-945d-11ea-a64c-ebeb45b9d295#webhook.com",
"primaryContactInfo": [
{
"address": "integration-generic-a03293c0-945d-11ea-a64c-ebeb45b9d295#webhook.com",
"mediaType": "EMAIL",
"type": "PRIMARY"
}
],
"addresses": [],
"state": "active",
"username": "integration-generic-a03293c0-945d-11ea-a64c-ebeb45b9d295#webhook.com",
"version": 2,
"acdAutoAnswer": false,
"selfUri": "/api/v2/users/c5ce06dc-6265-4d16-be18-f5fc5a918295"
},
{
/** 3rd User *********/
}
{
/** 4th User *********/
}
],
"pageSize": 25,
"pageNumber": 1,
"total": 7,
"firstUri": "/api/v2/users?pageSize=25&pageNumber=1",
"selfUri": "/api/v2/users?pageSize=25&pageNumber=1",
"lastUri": "/api/v2/users?pageSize=25&pageNumber=1",
"pageCount": 1
}
Entities is an array of objects, but you are trying to refer to its properties as an object:
userRefID : userInfoResponse.entities.id, // but "entities": [ {"id":"...", "name":"..."} ],
userName: userInfoResponse.entities.name,
You have to collect the data in a loop, and only then insert it into the database:
const usersArray = userInfoResponse.entities.map(el => ({
userRefID : el.id,
userName: el.name,
divisionId: el.division.id,
divisionName: el.division.name,
emailId: el.primaryContactInfo[0].address
}));
getAllUsers.insertMany(usersArray)

"data" field not populated in axios response from express server

I am trying to access data from a nodejs server using Express on the server and Axios on the backend.
This is the endpoing I am trying to reach: http://gentle-bastion-49098.herokuapp.com/api/filters
As you can see it actually returns data when you navigate to it. But when I try to access it using the following code:
const BASE_URL = 'http://gentle-bastion-49098.herokuapp.com/api'
function getFilterData () {
const url = `${BASE_URL}/filters`
return axios.get(url)
}
getFilterData()
.then(function (response) {
console.log('filter', response)
})
.catch(err => {
alert('Could not get filters ' + err.message.toString())
})
I get this response with the "data" field being unpopulated where I'm expecting it to contain the JSON you see in the URL.
{
"data": "",
"status": 200,
"statusText": "OK",
"headers": {},
"config": {
"url": "http://gentle-bastion-49098.herokuapp.com/api/filters",
"method": "get",
"headers": {
"Accept": "application/json, text/plain, */*"
},
"transformRequest": [null],
"transformResponse": [null],
"timeout": 0,
"xsrfCookieName": "XSRF-TOKEN",
"xsrfHeaderName": "X-XSRF-TOKEN",
"maxContentLength": -1
},
"request": {}
}
Here is the back end code
const express = require('express');
const app = express();
const async = require('async');
const request = require('request');
const http = require('http');
const EventSource = require('eventsource');
const port = process.env.PORT || 8080;
const bodyParser = require('body-parser');
const jsonParser = bodyParser.json()
app.get('/api/filters', function(req, res) {
let filtersResponse = {
"ID": "CONV_DATA#IVA",
"ApplicationName": "InterationsView",
"Type": "FILT_DETAIL",
"filters": [{
"Name": "ChannelType",
"Values": uniqueFilters.ChannelType,
},
{
"Name": "sessionType",
"Values": uniqueFilters.sessionType,
},
{
"Name": "Direction",
"Values": uniqueFilters.Direction,
},
{
"Name": "Status",
"Values": uniqueFilters.Status,
},
{
"Name": "statusReason",
"Values": uniqueFilters.statusReason,
},
],
"minDuration": uniqueFilters.minDuration,
"maxDuration": uniqueFilters.maxDuration,
"minData": "2019-08-29T22:28:47.029UTC",
"maxDate": "2019-08-29T22:28:49.578UTC"
};
// Respond with filters
res.json(filtersResponse);
});
Any ideas as to why the data field is unpopulated even though when accessed through browser or postman it returns the desired data? Is it a problem with the back end or the way the request is being made? Thanks.
I have also enabled cross-orgin resource sharing on my browser. Not doing so results in an error
I am not clear whether you are not getting axios response or response from your node server. If you have problem in getting axios response here is the code.
I have used request npm for making a get request.
const request = require('request');
apiUrl = "http://gentle-bastion-49098.herokuapp.com/api/filters"
request.get(
{
url: apiUrl,
json: true
},
function (error, response, body) {
if (error) {
console.log("Error Occurred :", error);
}
console.log("Response Data :", body)
}
);
The above code will give you response as :
{
"ID":"CONV_DATA#IVA",
"ApplicationName":"InterationsView",
"Type":"FILT_DETAIL",
"filters":[
{
"Name":"ChannelType",
"Values":[
"Phone",
"Web-Chat",
"Google-Assistant"
]
},
{
"Name":"sessionType",
"Values":[
"nlu-voice",
"nlu-text"
]
},
{
"Name":"Direction",
"Values":[
"In"
]
},
{
"Name":"Status",
"Values":[
"Complete",
"Started"
]
},
{
"Name":"statusReason",
"Values":[
"END"
]
}
],
"minDuration":9.7,
"maxDuration":154.2,
"minData":"2019-08-29T22:28:47.029UTC",
"maxDate":"2019-08-29T22:28:49.578UTC"
}
which is same as what you get in browser when you visit the link http://gentle-bastion-49098.herokuapp.com/api/filters
If you are using axios the code will be :
const axios = require('axios');
apiUrl = "http://gentle-bastion-49098.herokuapp.com/api/filters"
axios.get(apiUrl)
.then(function (response) {
console.log("Response Data :", response.data);
})
.catch(function (error) {
console.log("Error Occurred :", error);
})
and it will give same response as above.
Even your written code is giving response :
Try with these changes:
getFilterData().then(response => {
console.log('filter', response.data)
})
.catch(err => {
alert('Could not get filters ' + err.message.toString())
})
In your server code, send the response back to client using res.send() as shown below:
app.get('/api/filters', function(req, res) {
let filtersResponse = {
"ID": "CONV_DATA#IVA",
"ApplicationName": "InterationsView",
"Type": "FILT_DETAIL",
"filters": [{
"Name": "ChannelType",
"Values": uniqueFilters.ChannelType,
},
{
"Name": "sessionType",
"Values": uniqueFilters.sessionType,
},
{
"Name": "Direction",
"Values": uniqueFilters.Direction,
},
{
"Name": "Status",
"Values": uniqueFilters.Status,
},
{
"Name": "statusReason",
"Values": uniqueFilters.statusReason,
},
],
"minDuration": uniqueFilters.minDuration,
"maxDuration": uniqueFilters.maxDuration,
"minData": "2019-08-29T22:28:47.029UTC",
"maxDate": "2019-08-29T22:28:49.578UTC"
};
// Respond with filters
res.send(
filtersResponse
)
});

API.ai Actions on Google - Failed to parse JSON response string with 'INVALID_ARGUMENT' error: ": Cannot find field."

This error is similar to what I asked here, but this time it's with NodeJs client.
I am trying to find directions to a location. As soon as the intent is triggered on my webhook, I am calculating the directions using GoogleMapAPI. But before it can finish and send a response, I receive the error on my Actions Console. I checked total response time and it is less than 2 seconds which is less than 5 seconds timeout by Google.Where I am wrong???
My API.ai Intent
Using express.js with Action-on-Google Node Client
'use strict';
const express = require('express');
const bodyParser = require('body-parser');
const intentHandler = require('./intent_handler')
const app = express();
app.use(bodyParser.json());
const ApiAiAssistant = require('actions-on-google').ApiAiAssistant;
// Create functions to handle requests here
....
....
const DIRECTION_INTENT = 'action_direction';
function MyAssistant(req, res) {
const assistant = new ApiAiAssistant({request: req, response: res});
assistant.handleRequest(responseHandler(assistant));
}
function responseHandler (assistant) {
// intent contains the name of the intent you defined in the Actions area of API.AI
let intent = assistant.getIntent();
switch (intent) {
case WELCOME_INTENT:
...
break;
case WELCOME_FALLBACK_PERMISSION_INTENT:
...
break;
case DIRECTION_INTENT:
console.log(">>>>>>>DIRECTION_INTENT<<<<<<<");
intentHandler.directionIntent(assistant);
break;
}
}
app.post('/', function (req, res) {
MyAssistant(req, res);
});
app.listen(8080, function () {
console.log('app listening on port 8080!')
});
Handler Code
'use strict';
const speech = require("./speech_template");
const direction = require("./directionModule");
const intent_handler = {
'welcomeIntent': function (assistant) {
.....
},
'welcomeFallbackPermissionIntent': function (assistant) {
.....
},
'directionIntent':function (assistant) {
console.log('direction intent');
direction.getDirectionWithSavedAddress(function (response) {
assistant.ask(response);
});
}
};
module.exports = intent_handler;
Direction Extraction --- ERROR comes on Action Console before this get finished
'use strict';
const striptags = require('striptags');
const speech = require("./speech_template");
let googleMapsClient = require('#google/maps').createClient({
key: global.GOOGLE_DIRECTION_KEY
});
const directionModule = {
'getDirectionWithSavedAddress': function (eventCallback) {
let myAdd = <From Saved Data>;
if (myAdd === undefined) {
console.log("error......");
}
let destination = <From Saved Data>;
this.getDirectionWithAddress(myAdd, destination, function (dir) {
....
if(SUCCESS){
eventCallback(`<speak> ${steps} </speak>`);
}else{
eventCallback(`<speak> ${speech.ERROR_DIRECTIONS} </speak>`);
}
});
},
'getDirectionWithAddress': function (add1, add2, eventCallback) {
let dir = {};
googleMapsClient.directions({
origin: add1,
destination: add2,
mode: "driving",
departure_time: "now"
}, function (err, response) {
if (!err) {
console.log(response.json.routes[0]);
....
....
....
} else {
console.log(`Error --> ${err.toString()}`);
....
}
eventCallback(dir);
});
}
};
module.exports = directionModule;
UPDATE
I am running the code locally via WebStorm and exposing webhook via port forwarding using ngrok.
Update2
BAD REQUEST 400
{
"originalRequest": {
"source": "google",
"version": "2",
"data": {
"isInSandbox": true,
"surface": {
"capabilities": [
{
"name": "actions.capability.AUDIO_OUTPUT"
}
]
},
"inputs": [
{
"rawInputs": [
{
"query": "get me there",
"inputType": "VOICE"
}
],
"arguments": [
{
"rawText": "get me there",
"textValue": "get me there",
"name": "text"
}
],
"intent": "actions.intent.TEXT"
}
],
"user": {
"locale": "en-US",
"userId": "<uID>"
},
"device": {},
"conversation": {
"conversationId": "<cID>",
"type": "ACTIVE",
"conversationToken": "[\"_actions_on_google_\",\"defaultwelcomeintent-followup\"]"
}
}
},
"id": "<ID>",
"timestamp": "2017-09-12T17:08:10.321Z",
"lang": "en",
"result": {
"source": "agent",
"resolvedQuery": "get me there",
"speech": "",
"action": "action_direction",
"actionIncomplete": false,
"parameters": {},
"contexts": [
{
"name": "_actions_on_google_",
"parameters": {},
"lifespan": 99
},
{
"name": "google_assistant_input_type_voice",
"parameters": {},
"lifespan": 0
},
{
"name": "actions_capability_audio_output",
"parameters": {},
"lifespan": 0
},
{
"name": "defaultwelcomeintent-followup",
"parameters": {},
"lifespan": 4
}
],
"metadata": {
"intentId": "<iID>",
"webhookUsed": "true",
"webhookForSlotFillingUsed": "false",
"nluResponseTime": 15,
"intentName": "DirectionIntent"
},
"fulfillment": {
"speech": "",
"messages": [
{
"type": 0,
"speech": ""
}
]
},
"score": 1
},
"status": {
"code": 200,
"errorType": "success"
},
"sessionId": "<sID>"
}
This looks like before my callback is finished, my webhook is sending empty response to Google Actions.
Why is this happening and How to resolve it?????
The problem lies in how your directionIntent() function calls, and handles the result of, your getDirectionWithSavedAddress() function. It expects getDirectionWithSavedAddress() returns a function, when it does not. Instead, getDirectionWithSavedAddress() expects to send its results to a callback.
So after it makes its call to getDirectionWithAddress(), the function ends, returning nothing. This "nothing" is sent to assistant.ask(), which returns that to Google's server. This is an invalid response, so you're getting the error.
Fixing this should be straightforward. You need to call getDirectionWithSavedAddress() with a callback function. Inside this function you should call assistant.ask() with the value sent to the callback.
So directionIntent() might look something like
'directionIntent':function (assistant) {
console.log('direction intent');
direction.getDirectionWithSavedAddress( function( msg ){
assistant.ask( msg );
} );
}
Updated
This line makes no sense:
assistant.handleRequest(responseHandler(assistant));
The assistant.handleRequest() function is supposed to be passed a Map of Intent names to functions to call to handle the event. You're doing this manually in the responseHandler() function and you're not returning a Map. Since you're not returning a Map, it fails when trying to do the handleRequest() and generates the error "Action Error: Request handler can NOT be empty".
You can fix this by just calling responseHandler(assistant) and not dealing with handleRequest() at all. Or you can create the map that handleRequest() is expecting and get rid of responseHandler() completely.

Resources