Create and return a Meteor method promise

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];
      });
    },
  });
}

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))

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.

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');
    }
  }
});
1 Like

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');
      }
    },
  });
}

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) {

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.

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

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)?

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 …

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.

2 Likes

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

1 Like

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');
}
1 Like

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

1 Like

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

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)

3 Likes