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...
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! 😀