Search code examples
zapierzapier-cli

Zapier Cli - Unhandled error


I’m creating an app that (for now) takes a file as input and returns its hash (sha256).
It works fine as long as the user use a file as input, but if he puts something else (a string for example), the application silently fails (there is a stack trace in the app’s logs but Zapier displays nothing particular) and returns a wrong hash.

I do no have the impression that the error is handleable by my code and the stack is pretty obfuscated:

┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│     Log       │ Unhandled error: Error: Error: Could not find the method to call: authentication.test                                            │
│               │ What happened:                                                                                                                   │
│               │   Executing authentication.test with bundle                                                                                      │
│               │   Error: Could not find the method to call: authentication.test                                                                  │
│               │ Error: Error: Could not find the method to call: authentication.test                                                             │
│               │     at execute (:censored:9:d1ba0cf2aa:/node_modules/zapier-platform-core/src/execute.js:83:11)                                  │
│               │     at input (:censored:9:d1ba0cf2aa:/node_modules/zapier-platform-core/src/create-command-handler.js:26:12)                     │
│               │     at Object.beforeMiddleware.then.newInput (:censored:9:d1ba0cf2aa:/node_modules/zapier-platform-core/src/middleware.js:90:22) │
│               │     at bound (domain.js:280:14)                                                                                                  │
│               │     at Object.runBound (domain.js:293:12)                                                                                        │
│               │     at Object.tryCatcher (:censored:9:d1ba0cf2aa:/node_modules/bluebird/js/release/util.js:16:23)                                │
│               │     at Promise._settlePromiseFromHandler (:censored:9:d1ba0cf2aa:/node_modules/bluebird/js/release/promise.js:512:31)            │
│               │     at Promise._settlePromise (:censored:9:d1ba0cf2aa:/node_modules/bluebird/js/release/promise.js:569:18)                       │
│               │     at Promise._settlePromise0 (:censored:9:d1ba0cf2aa:/node_modules/bluebird/js/release/promise.js:614:10)                      │
│               │     at Promise._settlePromises (:censored:9:d1ba0cf2aa:/node_modules/bluebird/js/release/promise.js:693:18)                      │
│               │     at Async._drainQueue (:censored:9:d1ba0cf2aa:/node_modules/bluebird/js/release/async.js:133:16)                              │
│               │     at Async._drainQueues (:censored:9:d1ba0cf2aa:/node_modules/bluebird/js/release/async.js:143:10)                             │
│               │     at Immediate.Async.drainQueues (:censored:9:d1ba0cf2aa:/node_modules/bluebird/js/release/async.js:17:14)                     │
│               │     at runCallback (timers.js:672:20)                                                                                            │
│               │     at tryOnImmediate (timers.js:645:5)                                                                                          │
│               │     at processImmediate [as _immediateCallback] (timers.js:617:5)                                                                │
│     Version   │ 1.0.7                                                                                                                            │
│     Step      │                                                                                                                                  │
│     Timestamp │ 2018-05-24T03:57:57-05:00                                                                                                        │
└───────────────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

My code is essentially a copy of the "Files" Example App with the creates/uploadFile.js file replaced by:

const request = require('request');
const crypto = require('crypto');

function hashStream(stream) {
    const hash = crypto.createHash('sha256').setEncoding('hex');
    return new Promise((resolve, reject) => {
        stream.pipe(hash)
            .on('finish', () => resolve(hash.read()))
            .on('error', reject)
    })
}

function hashFile(z, bundle) {
    const fileStream = request(bundle.inputData.file);
    return hashStream(fileStream).then((hash) => ({hash}));
};

module.exports = {
    key: 'hashFile',
    noun: 'File',
    display: {
        label: 'Hash File',
        description: 'Performs a sha256 on file.'
    },
    operation: {
        inputFields: [
            {key: 'file', required: true, type: 'file', label: 'File'},
        ],
        perform: hashFile,
        sample: { filename: 'example.pdf' },
        outputFields: [
            {key: 'hash', type: 'string', label: 'Hash'}
        ],
    }
};

Update: I eventually found my mistake: I was assuming that the request function would throw an error upon failure.

So the hashFile function were certainly hashing the error page.

Changing the hashFile function solved the problem:

function hashFile(z, bundle) {
    return new Promise((resolve, reject) => {
        request(bundle.inputData.file)
            .on('response', function ({statusCode}) {
                if (200 === statusCode) {
                    hashStream(this).then((hash) => resolve({hash}));
                } else {
                    reject(new Error(`Invalid status code ${statusCode}`));
                }
            })
            .on('error', (err) => reject(err))
    })
}

But: I'm not able to catch "Unhandled errors"; I tried with

process.on('unhandledRejection', (reason, p) => {
    console.log('Unhandled Rejection at:', p, 'reason:', reason);
    process.exit(1);
});

but I think that the Zapier engine prevents that kind of tricks because it has no effect...


Solution

  • David here, from the Zapier Platform team. There's no magic or tricks going on, but it looks like you're making life a little bit harder for yourself. Much of the zapier code doesn't support that event pattern (.on('x', function(){...)). Instead, everything uses promises already. Unhandled rejection refers to a rejected promise that you're not handling with a catch clause. We also provide a z.request function which is going to flow a little better than using the request package itself (though this is allowed if you want to!). Check out the doc section about making HTTP requests. We've also got a z.hash(algo, string) method. and then update your code to be something like the following:

    function hashFile(z, bundle) {
        return z.request(bundle.inputData.file).then(response => {
          if (response.code !== 200) {
            throw new Error('something bad')
          }
          // might need to pull something else out of the response 
          // if response.content isn't the string contents of the file
          return {hash: z.hash('sha256', response.content)}
        })
    };
    

    We've also made it pretty easy to unit test your code - if it works locally, it'll work on our servers as well. Again, no magic! 😀