Create and return a Meteor method promise


#1

I have a server side method for authenticating against a Microsoft Active Directory server using LDAP. This works nicely, but I can’t get the result back to the client - apparently the bind call is async and hence I am returning before the response is back. So as I understand Meteor methods, I should simply return a Promise and that will then be resolved by the Meteor call. How to do that?

if (Meteor.isServer) {
  Meteor.methods({
    /**
     * LDAP authentication method.
     *
     * @param  {String}  url LDAP server url.
     * @param  {String}  dn  distinguished name.
     * @param  {String}  email LDAP binddn.
     * @param  {String}  password LDAP password.
     */
    'ldap.authenticate': (url, dn, email, password) => {
      const client = ldap.createClient({ url });

      client.bind(email, password, (err, res) => {
        if (err) {
          console.log(err);
        }

        console.log(res);

        return [err, res];
      });
    },
  });
}

#2

Hi @maasha,

You can try something like this

if (Meteor.isServer) {
  Meteor.methods({
    /**
     * LDAP authentication method.
     *
     * @param  {String}  url LDAP server url.
     * @param  {String}  dn  distinguished name.
     * @param  {String}  email LDAP binddn.
     * @param  {String}  password LDAP password.
     */
    'ldap.authenticate': async (url, dn, email, password) => {
      return await clientBind(url, email, password);
    },
  });
}

async function clientBind(url, email, password) {
  const client = ldap.createClient({ url });

  return new Promise((resolve, reject) => {
    client.bind(email, password, (error, res) => {
      if (error) {
        reject(error);
      } else {
        resolve(res);
      }
    });
  });
}

If that doesnt work you can use return Promise.await(clientBind(url, email, password))


#3

So this both suggestions yield:

W20181207-15:47:36.517(1)? (STDERR) (node:28423) UnhandledPromiseRejectionWarning: RangeError: Maximum call stack size exceeded
W20181207-15:47:36.569(1)? (STDERR)     at Object.keys.forEach.key (packages/ejson/ejson.js:594:27)
W20181207-15:47:36.569(1)? (STDERR)     at Array.forEach (<anonymous>)
W20181207-15:47:36.569(1)? (STDERR)     at Object.EJSON.clone.v [as clone] (packages/ejson/ejson.js:594:18)
W20181207-15:47:36.570(1)? (STDERR)     at Object.keys.forEach.key (packages/ejson/ejson.js:595:22)
W20181207-15:47:36.570(1)? (STDERR)     at Array.forEach (<anonymous>)
W20181207-15:47:36.570(1)? (STDERR)     at Object.EJSON.clone.v [as clone] (packages/ejson/ejson.js:594:18)
W20181207-15:47:36.570(1)? (STDERR)     at Object.keys.forEach.key (packages/ejson/ejson.js:595:22)
W20181207-15:47:36.570(1)? (STDERR)     at Array.forEach (<anonymous>)
W20181207-15:47:36.570(1)? (STDERR)     at Object.EJSON.clone.v [as clone] (packages/ejson/ejson.js:594:18)
W20181207-15:47:36.570(1)? (STDERR)     at Object.keys.forEach.key (packages/ejson/ejson.js:595:22)
W20181207-15:47:36.570(1)? (STDERR)     at Array.forEach (<anonymous>)
W20181207-15:47:36.571(1)? (STDERR)     at Object.EJSON.clone.v [as clone] (packages/ejson/ejson.js:594:18)
W20181207-15:47:36.571(1)? (STDERR)     at Object.keys.forEach.key (packages/ejson/ejson.js:595:22)
W20181207-15:47:36.571(1)? (STDERR)     at Array.forEach (<anonymous>)
W20181207-15:47:36.571(1)? (STDERR)     at Object.EJSON.clone.v [as clone] (packages/ejson/ejson.js:594:18)
W20181207-15:47:36.571(1)? (STDERR)     at Object.keys.forEach.key (packages/ejson/ejson.js:595:22)
W20181207-15:47:36.571(1)? (STDERR) (node:28423) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 2)
W20181207-15:47:36.571(1)? (STDERR) (node:28423) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

LDAP search using ldapjs
#4

You don’t need to wrap this in a Promise and use async/await.

You can just use Meteor.wrapAsync:

Meteor.methods({
  /**
   * LDAP authentication method.
   *
   * @param  {String}  url LDAP server url.
   * @param  {String}  dn  distinguished name.
   * @param  {String}  email LDAP binddn.
   * @param  {String}  password LDAP password.
   */
  'ldap.authenticate'(url, dn, email, password) {
    const client = ldap.createClient({ url });
    const clientBind = Meteor.wrapAsync(client.bind, client);
    try {
      const res = clientBind(email, password);
      console.log(res);
      return res;
    } catch (err) {
      throw new Meteor.Error('xxx', 'yyy');
    }
  }
});

#5

OK, now I get the BindResponse printed to console server side, but then I get:

W20181207-16:21:21.565(1)? (STDERR) (node:28726) UnhandledPromiseRejectionWarning: RangeError: Maximum call stack size exceeded
W20181207-16:21:21.565(1)? (STDERR)     at Object.keys.forEach.key (packages/ejson/ejson.js:594:27)
W20181207-16:21:21.565(1)? (STDERR)     at Array.forEach (<anonymous>)
W20181207-16:21:21.565(1)? (STDERR)     at Object.EJSON.clone.v [as clone] (packages/ejson/ejson.js:594:18)
W20181207-16:21:21.565(1)? (STDERR)     at Object.keys.forEach.key (packages/ejson/ejson.js:595:22)
W20181207-16:21:21.566(1)? (STDERR)     at Array.forEach (<anonymous>)
W20181207-16:21:21.566(1)? (STDERR)     at Object.EJSON.clone.v [as clone] (packages/ejson/ejson.js:594:18)
W20181207-16:21:21.566(1)? (STDERR)     at Object.keys.forEach.key (packages/ejson/ejson.js:595:22)
W20181207-16:21:21.566(1)? (STDERR)     at Array.forEach (<anonymous>)
W20181207-16:21:21.566(1)? (STDERR)     at Object.EJSON.clone.v [as clone] (packages/ejson/ejson.js:594:18)
W20181207-16:21:21.566(1)? (STDERR)     at Object.keys.forEach.key (packages/ejson/ejson.js:595:22)
W20181207-16:21:21.566(1)? (STDERR)     at Array.forEach (<anonymous>)
W20181207-16:21:21.566(1)? (STDERR)     at Object.EJSON.clone.v [as clone] (packages/ejson/ejson.js:594:18)
W20181207-16:21:21.567(1)? (STDERR)     at Object.keys.forEach.key (packages/ejson/ejson.js:595:22)
W20181207-16:21:21.567(1)? (STDERR)     at Array.forEach (<anonymous>)
W20181207-16:21:21.567(1)? (STDERR)     at Object.EJSON.clone.v [as clone] (packages/ejson/ejson.js:594:18)
W20181207-16:21:21.567(1)? (STDERR)     at Object.keys.forEach.key (packages/ejson/ejson.js:595:22)
W20181207-16:21:21.567(1)? (STDERR) (node:28726) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 5)

Client:

handleSubmit(event) {
    const that = this;
    event.preventDefault();
    const email = document.getElementById('login-email').value;
    const password = document.getElementById('login-password').value;

    Meteor.call(
      'ldap.authenticate',
      '<url>',
      '<dn>',
      email,
      password,
      (err, res) => {
        if (err) {
          that.setState({ error: err });
          that.setState({ authenticated: false });
          throw new Error('ldap.authenticate failed');
        }

        console.log('ldap.authenticate', res);

        that.setState({ authenticated: true });
      },
    );
}

Server:

if (Meteor.isServer) {
  Meteor.methods({
    /**
     * LDAP authentication method.
     *
     * @param  {String}  url LDAP server url.
     * @param  {String}  dn  distinguished name.
     * @param  {String}  email LDAP binddn.
     * @param  {String}  password LDAP password.
     */
    'ldap.authenticate': (url, dn, email, password) => {
      const client = ldap.createClient({ url });
      const clientBind = Meteor.wrapAsync(client.bind, client);
      try {
        const res = clientBind(email, password);
        console.log(res);
        return res;
      } catch (err) {
        throw new Meteor.Error('xxx', 'yyy');
      }
    },
  });
}

#6

You have something in an infinite loop.

Also, you should not use fat arrow syntax for Meteor methods (I don’t think that’s causing your problem, though).

Replace

'ldap.authenticate': (url, dn, email, password) => {

with

'ldap.authenticate'(url, dn, email, password) {

#7

OK, now using object shorthand instead of fat arrow syntax. If the credentials are bad I get the expected error. If the credentials are good, I get the pesky loop.


#8

:o( I am stuck with this loop. Seems to be a Meteor server thing. Suggestions most welcome!


#9

There’s nothing obvious from the code you’ve shared which would explain that behaviour.

Perhaps start by introducing some logging to isolate where the loop is being executed: for example, is the client calling the server repeatedly (the method is being re-run)?


#10

A console.log client side indicated that the method is invoked once. If I do this server side:

if (Meteor.isServer) {
  Meteor.methods({
    /**
     * LDAP authentication method.
     *
     * @param  {String}  url LDAP server url.
     * @param  {String}  dn  distinguished name.
     * @param  {String}  email LDAP binddn.
     * @param  {String}  password LDAP password.
     */
    'ldap.authenticate'(url, dn, email, password) {
      console.log('1111')
      const client = ldap.createClient({ url });
      console.log('2222')
      const clientBind = Meteor.wrapAsync(client.bind, client);
      console.log('3333')

      try {
        console.log('4444')
        const res = clientBind(email, password);
        console.log('5555')
        console.log(res);
        return res;
      } catch (err) {
        console.log('6666')
        throw new Error(`bind failed: ${err}`);
      }
    },
  });
}

I get

I20181210-12:51:52.698(1)? 1111
I20181210-12:51:52.701(1)? 2222
I20181210-12:51:52.701(1)? 3333
I20181210-12:51:52.702(1)? 4444
I20181210-12:51:52.794(1)? 5555

as well as the Bind response that looks ok.

If I change the return statement to return a string it is fine, but return(res) causes the loop …


#11

Right.

I tried this:

const str = JSON.stringify(res);

And it gives me:

TypeError: Converting circular structure to JSON

bla bla bla

So that circular thing is probably resulting in the loop.


#12

So returning true on successful bind solves all problems. Thanks rob!


#13

Are you using ldapjs?

I just looked at the docs for bind, and the res parameter is not documented, so I suspect it’s just returning client, which may well contain circular references.

As res is not returned, change the code (as I guess you’ve done) to this:

try {
  clientBind(email, password);
  return true;
} catch (err) {
  throw new Meteor.Error('xxx', 'yyy');
}

#14

Yes, this is ldapjs and I ended up doing just like you suggested :o)


#15

Yeah you can’t return circular structures from a method because it needs to be parsed into EJSON.
Sadly the stringifier for ejson doesn’t tell you when you’re parsing a circular structure, unlike JSON.stringify


#16

I’ve opened an issue to get ejson to throw more helpful errors here: https://github.com/meteor/meteor/issues/10373

It should be a pretty simple change, so I’ll whip up a PR sometime in the next month (if nobody else jumps in and does it first)