2

I'm having an issue with Callable Functions. Here's my Firebase function:

const functions = require('firebase-functions');
const admin = require('firebase-admin');
const request = require('request-promise');

admin.initializeApp(functions.config().firebase);

exports.phoneAuthRequest = functions.https.onCall((data, context) => {
  // Message text passed from the client.
  const text = data.text;

  // Authentication / user information is automatically added to the request.
  const uid = context.auth.uid;
  const phone = context.auth.token.phone_number;

  const url = "https://api.authy.com/protected/json/phones/verification/start?api_key=<key here>&via=sms&country_code=1&phone_number=" + phone;
  const httpReq = {
    uri: url,
    method: 'POST',
    json: true,
    resolveWithFullResponse: true
  };

  return new Promise((resolve, reject) => {
    request(httpReq).then((response) => {
      // Twillio request returned with success
      console.log("response: " + JSON.stringify(response));
      return resolve(response);
    }).catch(function(error) {
      console.log("error: " + JSON.stringify(error));
      return reject(error);
    });  
  });
});

And here is my client example:

<html>
  <head>
    <script src="https://www.gstatic.com/firebasejs/5.4.2/firebase-app.js"></script>
    <script src="https://www.gstatic.com/firebasejs/5.4.2/firebase-auth.js"></script>
    <script src="https://www.gstatic.com/firebasejs/5.4.2/firebase-database.js"></script>
    <script src="https://www.gstatic.com/firebasejs/5.4.2/firebase-functions.js"></script>

    <script>
      var config = { <firebase configuration in here> };
      firebase.initializeApp(config);

      function myTest() {
        var display = document.getElementById('display');
        display.innerHTML = "Started";

        var auth = firebase.auth();
        auth.signInWithEmailAndPassword(<username>,<password>).then(function(authResult) {
          display.innerHTML = "Signed in";

          var phoneAuthRequest = firebase.functions().httpsCallable('phoneAuthRequest');
          phoneAuthRequest({'text': 'Test'}).then(function(result) {
            display.innerHTML = JSON.stringify(result);
          }).catch(function(error) {
            display.innerHTML = JSON.stringify(error);
          });
        }).catch(function(error) {
          display.innerHTML = "Login failure.  Your email or password could not be validated.  " + JSON.stringify(error);
        });
      }
    </script>
  </head>

  <body>
    <input type="submit" value="Test It" data-wait="Loading..." id="loginBtn" class="login-window-login-btn w-button" onclick="myTest();">
    <div id="display"></div>
  </body>
</html>

A test of this code combination shows that the Firebase function 'phoneAuthRequest' gets called, which in turn makes the request to Twillio, and the response from Twillio is returned correctly in response, but the Console Logs show this error:

Unhandled error RangeError: Maximum call stack size exceeded
    at Function.mapValues (/user_code/node_modules/firebase-functions/node_modules/lodash/lodash.js:13395:23)
    at encode (/user_code/node_modules/firebase-functions/lib/providers/https.js:242:18)
    at /user_code/node_modules/firebase-functions/node_modules/lodash/lodash.js:13400:38
    at /user_code/node_modules/firebase-functions/node_modules/lodash/lodash.js:4925:15
    at baseForOwn (/user_code/node_modules/firebase-functions/node_modules/lodash/lodash.js:3010:24)
    at Function.mapValues (/user_code/node_modules/firebase-functions/node_modules/lodash/lodash.js:13399:7)
    at encode (/user_code/node_modules/firebase-functions/lib/providers/https.js:242:18)
    at /user_code/node_modules/firebase-functions/node_modules/lodash/lodash.js:13400:38
    at /user_code/node_modules/firebase-functions/node_modules/lodash/lodash.js:4925:15
    at baseForOwn (/user_code/node_modules/firebase-functions/node_modules/lodash/lodash.js:3010:24)
    at Function.mapValues (/user_code/node_modules/firebase-functions/node_modules/lodash/lodash.js:13399:7)
    at encode (/user_code/node_modules/firebase-functions/lib/providers/https.js:242:18)
    at /user_code/node_modules/firebase-functions/node_modules/lodash/lodash.js:13400:38
    at /user_code/node_modules/firebase-functions/node_modules/lodash/lodash.js:4925:15
    at baseForOwn (/user_code/node_modules/firebase-functions/node_modules/lodash/lodash.js:3010:24)
    at Function.mapValues (/user_code/node_modules/firebase-functions/node_modules/lodash/lodash.js:13399:7)
    at encode (/user_code/node_modules/firebase-functions/lib/providers/https.js:242:18)
    at /user_code/node_modules/firebase-functions/node_modules/lodash/lodash.js:13400:38
    at /user_code/node_modules/firebase-functions/node_modules/lodash/lodash.js:4925:15
    at baseForOwn (/user_code/node_modules/firebase-functions/node_modules/lodash/lodash.js:3010:24)
    at Function.mapValues (/user_code/node_modules/firebase-functions/node_modules/lodash/lodash.js:13399:7)
    at encode (/user_code/node_modules/firebase-functions/lib/providers/https.js:242:18)

And of course, the response back at the client is:

{"code":"internal"}

As I read similar posts, the advice was to serialize the promises, which I'm not sure I totally understand how to do, or, to wrap the async call (request in this case) inside of a new promise, which is exactly what I have done here, and it still doesn't work.

If you are going to answer this question, can you please be specific about the proposed solution by showing a code example?

Thanks in advance...

Jaromanda X
  • 53,868
  • 5
  • 73
  • 87
Randall Sargent
  • 171
  • 1
  • 1
  • 7
  • is there more information to the error? Errors usually include some sort of trace as to where the error occurred - which `console.log` outputs the error? – Jaromanda X Sep 09 '18 at 22:33
  • I have updated the main thread to include the full error. There is no question that the promise functions are looping in some capacity, probably due to a coding error. I just need to understand what I'm doing wrong, and how to fix it. – Randall Sargent Sep 10 '18 at 02:27
  • is the error in the client or server side? – Jaromanda X Sep 10 '18 at 02:44
  • The error is on the server. – Randall Sargent Sep 10 '18 at 11:15
  • Is `response:` logged at all? – Jaromanda X Sep 10 '18 at 11:52
  • Yes, I have added a Console.Log inside the request .then, and the response is as expected from Twillio, so the request is working, it’s just not returning back to the Client because of the stack overflow. – Randall Sargent Sep 10 '18 at 12:06
  • there is very much doubt it's a promise issue ... Promises don't use lodash or recurse infinitely - is `functions.https.onCall` expected to return a Promise? (I see that it can) ... try, just to test `resolve('[]')` just to see if it's the `resolve(response)` that causes the error – Jaromanda X Sep 10 '18 at 12:10
  • I have tried resolve([]). There's no change in behavior. – Randall Sargent Sep 10 '18 at 16:08

2 Answers2

1

Ok, I resolved this issue with a ton of knocking my head against the wall. Here's the code:

exports.phoneAuthRequest = functions.https.onCall((data, context) => {
  // Example of using context to obtain the user information about the calling user
  const uid = context.auth.uid;
  // Example of obtaining a paramter that was passed by the calling function
  const phone = data.phone;
  const e164 = "+1" + phone;

  httpReq = ...

  // Example of a return if you have not yet made a call to an asynchonous function.
  return ({'status': 'success'});

  // Here is an example of a nested set of asynchonous calls and how to return from them.
  return admin.auth().updateUser(uid, {'phoneNumber': e164}).then(function(userRecord) {
    // the Phone Number was updated
    return request(httpReq).then((response) => {
      var updates = {};
      updates['/users/' + uid + "/centralVerified"] = false;
      return database.ref().update(updates).then(function(rslt) {
        return Promise.resolve({'status': 'success');
      }).catch(function(error) {
        return Promise.reject({'status': 'failed'});
      });
    }).catch(function(error) {
      return Promise.reject({'status': 'failed'});
    });
  }).catch(function(error) {
    return Promise.reject({'status': 'failed'});
  });
});

Notice in the above that there are two distinct ways of returning from a callable function. If you haven't made any asynchonous calls, then you can simply return values, but if you have called an asynchonous function, you must return a promise. If you are looking for a Reject or Resolve in the calling routine, a return of values is assumed to be a Resolve.

I hope this helps others... Best of luck!

Randall Sargent
  • 171
  • 1
  • 1
  • 7
0

Try eliminating the client and call the Firebase function through curl.

Call functions via HTTP requests

chris
  • 184
  • 5
  • Respectfully, a Callable Function has no endpoint to call and is reliant upon the Client context. It is not possible to eliminate the client in this case. – Randall Sargent Sep 10 '18 at 11:19
  • There are multiple sources that say otherwise. [https://firebase.google.com/docs/functions/callable-reference] (https://firebase.google.com/docs/functions/callable-reference) [https://stackoverflow.com/questions/49476231/how-to-call-firebase-callable-functions-with-http] (https://stackoverflow.com/questions/49476231/how-to-call-firebase-callable-functions-with-http) – chris Sep 11 '18 at 16:50