Callback invocation for async methods

I’ve been reading excellent blog post by Rob Fallows: https://blog.meteor.com/using-promises-and-async-await-in-meteor-8f6f4a04f998

So i decided to switch our LDAP connection from wrapAsync to async/await syntax.

Looks like everything is working fine, except for callback invocation on a client.

Here is server code:

const LdapClient = Npm.require('promised-ldap')
Meteor.methods({
    'ldap.testAsyncConnection': async function(server, port, login, pass) {
        try {
            const url = `ldap://${server}:${port}`
            console.log('[methods.ldap.testAsyncConnection] creating client')

            const client = new LdapClient({ url })
            const dn = `cn=${login}`
            console.log('[methods.ldap.testAsyncConnection] binding')
            await client.bind(dn, pass)
            console.log('[methods.ldap.testAsyncConnection] binded')
            const searchOptions = {
                attributes: ['dn', 'sn', 'cn']
            }
            console.log('[methods.ldap.testAsyncConnection] going to search')
            const results = await client.search('<SEARCH HERE>', searchOptions)
            console.log('[methods.ldap.testAsyncConnection] here are search results! ', results)
            return {search: results}
        } catch (e) {
            console.warn('[methods.ldap.testAsyncConnection] exception, e = ', e)
            return { success: false, error: e }
        }
        console.log('[methods.ldap.testAsyncConnection] after try/catch')
    }
})

Here is client-side method invokation:

Meteor.call('ldap.testAsyncConnection', 'localhost', '6389', '<DN here>', '<pass here>', (err, res) => console.log(err, res))

Here is server log for that call:

I20170329-15:02:35.752(3)? [methods.ldap.testAsyncConnection] creating client
I20170329-15:02:35.757(3)? [methods.ldap.testAsyncConnection] binding
I20170329-15:02:35.765(3)? [methods.ldap.testAsyncConnection] binded
I20170329-15:02:35.765(3)? [methods.ldap.testAsyncConnection] going to search
I20170329-15:02:35.765(3)? [methods.ldap.testAsyncConnection] here are search results!  { entries: 
I20170329-15:02:35.767(3)?    [ SearchEntry {
I20170329-15:02:35.768(3)?        messageID: 2,
I20170329-15:02:35.768(3)?        protocolOp: 100,
I20170329-15:02:35.771(3)?        controls: [],
I20170329-15:02:35.771(3)?        log: [Object],
I20170329-15:02:35.771(3)?        objectName: '<NAME HERE>',
I20170329-15:02:35.788(3)?        attributes: [],
I20170329-15:02:35.789(3)?        connection: [Object] } ],
I20170329-15:02:35.789(3)?   references: [] }

So everything is working great, but the client callback is never run. And after server restarts method invocations run again (i see it in server logs). What am i doing wrong?

The problem may be with the way you’ve used the callback (as an expression body) in ES6 notation:

Meteor.call('ldap.testAsyncConnection', 'localhost', '6389', '<DN here>', '<pass here>', (err, res) => console.log(err, res))

which is equivalent to:

Meteor.call('ldap.testAsyncConnection', 'localhost', '6389', '<DN here>', '<pass here>', (err, res) => {
  return console.log(err, res))
});

And although that looks like it should work, browsers vary in the way they handle console.log - can you try the more explicit (statement body) form:

Meteor.call('ldap.testAsyncConnection', 'localhost', '6389', '<DN here>', '<pass here>', (err, res) => {
  console.log(err, res))
});

No, Rob, it doesn’t helps.
Also doesn’t helps change the end of method: return await client.search(...)

Looks like Meteor doesn’t like usual object that i return from async function. Can it be wating for Promise or something?

Environment:

OS X 10.12.3 (16D32)
Meteor METEOR@1.4.3.2

Installed packages:

meteor-base@1.0.4             # Packages every Meteor app needs to have
mobile-experience@1.0.4       # Packages for a great mobile UX
mongo@1.1.16                   # The database Meteor supports right now
blaze-html-templates@1.0.4 # Compile .html files into Meteor Blaze views
reactive-var@1.0.11            # Reactive variable for tracker
jquery@1.11.10                  # Helpful client-side library
tracker@1.1.2                 # Meteor's client-side reactive programming library

standard-minifier-css@1.3.4   # CSS minifier run for production mode
standard-minifier-js@1.2.3    # JS minifier run for production mode
es5-shim@4.6.15                # ECMAScript 5 compatibility for older browsers.
ecmascript@0.6.3              # Enable ECMAScript2015+ syntax in app code
shell-server@0.2.3            # Server-side component of the `meteor shell` command

fourseven:scss
wolves:bourbon
accounts-ui@1.1.9
accounts-password@1.3.4
reactrouter:react-router-ssr
url@1.1.0
react-meteor-data
alanning:roles
aldeed:simple-schema
aldeed:collection2
cfs:standard-packages
cfs:gridfs
cfs:graphicsmagick
session@1.1.7
momentjs:moment
mdg:geolocation
http@1.2.12
nimble:restivus
check@1.2.5

gms:meteor-jwt-login
dispatch:run-as-user
gms:gms-lib
gms:gms-ldap

No - the Promise is fully resolved on the server before it’s sent to the client.

Is the object EJSONable? If it contains functions, it won’t get serialized correctly.

Have you tried simplifying the returned data?

Oh God, you were right, it was some unserializable data. Thanks a lot, Rob!

BTW, it’d be nice for Meteor at least to console.warn such events (unserializable data in method’s return) to server logs.

2 Likes

Cool. Glad you got it figured out and thanks for the kind words about the article - much appreciated. :slight_smile:

1 Like

By the way, you can simplify your method definition to:

async 'ldap.testAsyncConnection'(server, port, login, pass) {
1 Like

I agree 100% with that.
Return something that is not serializable (like a cursor that you have forgotten to .fech(), ask me how i know), and the error will just be swallowed, and never return on the client, no error, nothing.

1 Like

I just want to reiterate that async/await is a real breeze after all these callbacks hells and chained then-then-then! The code is so clean now :))

async function getClient(options) {
    let client
    const { server, port, login, pass, base } = options
    const url = `ldap://${server}:${port}`
    try {
        client = new LdapClient({ url })
        const dn = `cn=${login},${base}`
        await client.bind(dn, pass)
        return { success: true, client }
    } catch (e) {
        console.warn('[ldap.getClient] exception, e = ', e)
        return { success: false, error: e }
    }
}

async function ldapSearch(client, login, base) {
    try {
        const search = `cn=${login},${base}`
        const results = await client.search(search, {})
        const entries = results.entries.map(entry => ({
            objectName: entry.objectName,
            attributes: attributesToJSON(entry.attributes)
        }))

        return { success: true, entries }
    } catch (e) {
        if (!(e instanceof ldapjs.NoSuchObjectError)) {
            console.warn('[ldap.ldapSearch] exception, e = ', e)
            return { success: false, error: e }
        }
        return { success: true, entries: [] }
    }
}

Meteor.methods({
    async 'ldap.findOrCreateUser' (options) {
        const { username, role, base } = options
        const loginName = cleanUsername(username)
        const user = Meteor.users.findOne({ username })
        if (!user)
            throw new Error('no user found')

        const res = await getClient(options)
        if (!res.success)
            return res
        const { client } = res

        const searchRes = await ldapSearch(client, loginName, base)
        if (!searchRes.success)
            return searchRes

        if (searchRes.success && searchRes.entries.length)
            return { success: true, user: searchRes.entries[0] }

        return await insertUser(client, user, role, base)
    }
})
1 Like