How to catch UnhandledPromiseRejectionWarning for GCS WriteStream - node.js

Observed Application Behavior
I'm getting a UnhandledPromiseRejectionWarning: Error: Upload failed when using #google-cloud/storage in node.js.
These errors come when processing thousands of requests. It's a small percentage that cause errors, but due to the lack of ability to handle the errors, and the lack of proper context from the error message, it's very difficult to determine WHICH files are failing.
I know in general promises must have a .catch or be surrounded by a try/catch block. But in this case I'm using a write stream. I'm a little bit confused as to where the promise that's being rejected is actually located and how I would intercept it. The stack trace is unhelpful, as it only contains library code:
UnhandledPromiseRejectionWarning: Error: Upload failed
at Request.requestStream.on.resp (.../node_modules/gcs-resumable-upload/build/src/index.js:163:34)
at emitTwo (events.js:131:20)
at Request.emit (events.js:214:7)
at Request.<anonymous> (.../node_modules/request/request.js:1161:10)
at emitOne (events.js:121:20)
at Request.emit (events.js:211:7)
at IncomingMessage.<anonymous> (.../node_modules/request/request.js:1083:12)
at Object.onceWrapper (events.js:313:30)
at emitNone (events.js:111:20)
at IncomingMessage.emit (events.js:208:7)
My Code
The code that's creating the writeStream looks like this:
const {join} = require('path')
const {Storage} = require('#google-cloud/storage')
module.exports = (config) => {
const storage = new Storage({
projectId: config.gcloud.project,
keyFilename: config.gcloud.auth_file
})
return {
getBucketWS(path, contentType) {
const {bucket, path_prefix} = config.gcloud
// add path_prefix if we have one
if (path_prefix) {
path = join(path_prefix, path)
}
let setup = storage.bucket(bucket).file(path)
let opts = {}
if (contentType) {
opts = {
contentType,
metadata: {contentType}
}
}
const stream = setup.createWriteStream(opts)
stream._bucket = bucket
stream._path = path
return stream
}
}
}
And the consuming code looks like this:
const gcs = require('./gcs-helper.js')
module.exports = ({writePath, contentType, item}, done) => {
let ws = gcs.getBucketWS(writePath, contentType)
ws.on('error', (err) => {
err.message = `Could not open gs://${ws._bucket}/${ws._path}: ${err.message}`
done(err)
})
ws.on('finish', () => {
done(null, {
path: writePath,
item
})
})
ws.write(item)
ws.end()
}
Given that I'm already listening for the error event on the stream, I don't see what else I can do here. There isn't a promise happening at the level of #google-cloud/storage that I'm consuming.
Digging into the #google-cloud/storage Library
The first line of the stack trace brings us to a code block in the gcs-resumable-upload node module that looks like this:
requestStream.on('complete', resp => {
if (resp.statusCode < 200 || resp.statusCode > 299) {
this.destroy(new Error('Upload failed'));
return;
}
this.emit('metadata', resp.body);
this.deleteConfig();
this.uncork();
});
This is passing the error to the destroy method on the stream. The stream is being created by the #google-cloud/common project's utility module, and this is using the duplexify node module to create the stream. The destroy method is defined on the duplexify stream and can be found in the README documentation.
Reading the duplexify code, I see that it first checks this._ondrain before emitting an error. Maybe I can provide a callback to avoid this error being unhandled?
I tried ws.write(item, null, cb) and still got the same UnhandledPromiseRejectionWarning. I tried ws.end(item, null, cb) and even wrapped the .end call in a try catch, and ended up getting this error which crashed the process entirely:
events.js:183
throw er; // Unhandled 'error' event
^
Error: The uploaded data did not match the data from the server. As a precaution, the file has been deleted. To be sure the content is the same, you should try uploading the file again.
at delete (.../node_modules/#google-cloud/storage/build/src/file.js:1295:35)
at Util.handleResp (.../node_modules/#google-cloud/common/build/src/util.js:123:9)
at retryRequest (.../node_modules/#google-cloud/common/build/src/util.js:404:22)
at onResponse (.../node_modules/retry-request/index.js:200:7)
at .../node_modules/teeny-request/build/src/index.js:208:17
at <anonymous>
at process._tickCallback (internal/process/next_tick.js:189:7)
My final code looks something like this:
let ws = gcs.getBucketWS(writePath, contentType)
const handleErr = (err) => {
if (err) err.message = `Could not open gs://${ws._bucket}/${ws._path}: ${err.message}`
done(err)
}
ws.on('error', handleErr)
// trying to do everything we can to handle these errors
// for some reason we still get UnhandledPromiseRejectionWarning
try {
ws.write(item, null, err => {
handleErr(err)
})
ws.end()
} catch (e) {
handleErr(e)
}
Conclusion
It's still a mystery to me how a user of the #google-cloud/storage library, or duplexify for that matter, is supposed to perform proper error handling. Comments from library maintainers of either project would be appreciated. Thanks!

Related

Proper way to handle fetch errors in NodeJS v18?

I switched to NodeJS v18 with the built-in fetch and I'm using it as such:
async function get511AK() {
let res = await fetch(URL, { method: 'GET' })
if (res.ok && (res.headers.get('content-type').includes('json'))) {
let data = await res.json();
jsonresponseAK = data;
} else {
console.log("(" + res.url + ') is not json');
}
}
However, sometimes I'm getting a timeout on the URL, which is going to happen, but it's causing the script to exit. I've tried wrapping this in try/catch and it did not prevent it from exiting.
This never happened in Node v12 under the node-fetch library. What else can I add to control those connection timeouts?
node:internal/deps/undici/undici:11118
Error.captureStackTrace(err, this);
^
TypeError: fetch failed
at Object.fetch (node:internal/deps/undici/undici:11118:11)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async Timeout.get511AK [as _onTimeout] (/home/wazebot/dot-scripts/script-relay.js:76:12) {
cause: ConnectTimeoutError: Connect Timeout Error
at onConnectTimeout (node:internal/deps/undici/undici:6625:28)
at node:internal/deps/undici/undici:6583:50
at Immediate._onImmediate (node:internal/deps/undici/undici:6614:13)
at process.processImmediate (node:internal/timers:471:21) {
code: 'UND_ERR_CONNECT_TIMEOUT'
}
}
Node.js v18.12.1
Hope it helped!
process.on('uncaughtException', console.log);
// Uncaught Exception thrown - when you throw an error and did not catch anywhere.
process.on('unhandledRejection', console.log);
// Unhandled Rejection at Promise - similar, when you fail to catch a Promise.reject.

creating custom token in cloud function after reteiving data from realtime database

i want to create custom token in cloud function but before that I want to check and compare timestamp from realtime database and with current time.if the timestamp is below 10 min then I want to create custom token and send back to client.please help me to achieve this.i am new to this cloud function firebase.
here is my code
export const authaccount = functions.https.onCall(async (data) => {
try {
const snap= await admin.database().ref("/register/"+data).get();
const time=snap.val().timestamp;
const now=new Date().getDate();
const reg=new Date(time).getDate();
const today=Math.abs(now-reg);
const daydiff=Math.floor(today/1000/60/60/24);
const nowminutes=new Date().getUTCMinutes();
const regminutes=new Date(time).getUTCMinutes();
const timediff=Math.abs(nowminutes-regminutes);
if (timediff<10 && daydiff==0) {
try {
admin.auth().createCustomToken(data).then((customtoken)=>{
console.log("auth created"+" "+timediff+" "+daydiff+" "+customtoken);
return customtoken;
});
} catch (err1) {
throw new functions.https.HttpsError("unknown", err1.message, err1);
}
} else {
console.log("else "+" "+now+" "+reg+" "+time+" "+daydiff);
}
} catch (err2) {
throw new functions.https.HttpsError("unknown", err2.message, err2);
}
});
2:53:20.626 AM
authaccount
Error: Process exited with code 16 at process.<anonymous> (/layers/google.nodejs.functions-framework/functions-framework/node_modules/#google-cloud/functions-framework/build/src/invoker.js:275:22) at process.emit (events.js:314:20) at process.EventEmitter.emit (domain.js:483:12) at process.exit (internal/process/per_thread.js:168:15) at Object.sendCrashResponse (/layers/google.nodejs.functions-framework/functions-framework/node_modules/#google-cloud/functions-framework/build/src/logger.js:37:9) at process.<anonymous> (/layers/google.nodejs.functions-framework/functions-framework/node_modules/#google-cloud/functions-framework/build/src/invoker.js:271:22) at process.emit (events.js:314:20) at process.EventEmitter.emit (domain.js:483:12) at processPromiseRejections (internal/process/promises.js:209:33) at processTicksAndRejections (internal/process/task_queues.js:98:32)
2:53:19.559 AM
authaccount
Error: The caller does not have permission; Please refer to https://firebase.google.com/docs/auth/admin/create-custom-tokens for more details on how to use and troubleshoot this feature. at FirebaseAuthError.FirebaseError [as constructor] (/workspace/node_modules/firebase-admin/lib/utils/error.js:44:28) at FirebaseAuthError.PrefixedFirebaseError [as constructor] (/workspace/node_modules/firebase-admin/lib/utils/error.js:90:28) at new FirebaseAuthError (/workspace/node_modules/firebase-admin/lib/utils/error.js:149:16) at Function.FirebaseAuthError.fromServerError (/workspace/node_modules/firebase-admin/lib/utils/error.js:188:16) at /workspace/node_modules/firebase-admin/lib/auth/token-generator.js:114:53 at processTicksAndRejections (internal/process/task_queues.js:97:5) at async Promise.all (index 1)
2:53:19.558 AM
authaccount
Unhandled rejection
2:53:19.469 AM
authaccount
Function execution took 1386 ms, finished with status code: 200
please help to solve this problem. i don't know where I am making the mistake.
You will need to make sure your Firebase Admin sdk is initiated and running before the function proceeds
if (firebase.apps.length === 0) {
firebase.initializeApp();
}
Resource: https://firebase.google.com/docs/admin/setup#initialize-without-parameters
I doubt you have modified the IAM permissions on your service account but as the comment suggested: https://firebase.google.com/docs/auth/admin/create-custom-tokens#service_account_does_not_have_required_permissions
Once that is confirmed to be working - you will need to ensure that the onCall data is a string and not null, some simple health checks can help you debug your process
console.log(typeof data);
console.warn("Data", data);
from there I would also debug your date times and the realtime database result, these are async and will require the promise to be resolved before you can use it.
Update:
All cloud functions should return a response to the client
onCall uses promises on the client and supports a 'return Object'
example:
return {
token: myCustomToken,
possible: otherValue
};
for comparison, onRequest uses fetch like responses and supports codes
response.status(500)
response.send({name:value})
return;
Source:
https://firebase.google.com/docs/functions/callable#sending_back_the_result
Source:
https://firebase.google.com/docs/functions/http-events#using_express_request_and_response_objects
Update:
all paths and promises need to resolve correctly, this includes awaiting promises to resolve and returning their result or storing the result for any secondary processing - I suggest cleaning up the code, remove the try/catch and use .then().catch()
Example:
if (timediff<10 && daydiff==0) {
return await admin.auth().createCustomToken(data)
.then((customtoken)=>{
console.log("auth created"+" "+timediff+" "+daydiff+" "+customtoken);
return customtoken;
})
.catch (err) {
return new functions.https.HttpsError("unknown", err.message, err);
}
}
else {
console.log("else "+" "+now+" "+reg+" "+time+" "+daydiff);
return "else "+" "+now+" "+reg+" "+time+" "+daydiff;
}

Google Cloud Function - Storage - Delete Image - "ApiError: Error during request"

UPDATED QUESTION
The problem is ApiError: Error during request.
Code:
import * as functions from 'firebase-functions';
const cors = require('cors')({ origin: true });
import * as admin from 'firebase-admin';
const gcs = admin.storage();
export const deleteImage = functions.https.onRequest((req, res) => {
return cors(req, res, async () => {
res.set('Content-Type', 'application/json');
const id = req.body.id;
const name = req.body.name;
const imageRef = gcs.bucket(`images/${name}`);
if (!name || !id) {
return res.status(400).send({message: 'Missing parameters :/'});
}
try {
await imageRef.delete();
console.log('Image deleted from Storage');
return res.status(200).send({status: 200, message: `Thank you for id ${id}`});
}
catch (error) {
console.log('error: ', error);
return res.status(500).send({message: `Image deletion failed: ${error}`});
}
});
});
And the problem is here: await imageRef.delete();, I get the following error:
ApiError: Error during request.
I do, indeed, have admin.initializeApp(); in one of my other functions, so that can't be the issue, unless GCF have a bug.
More In-Depth Error:
{ ApiError: Error during request.
at Object.parseHttpRespBody (/user_code/node_modules/firebase-admin/node_modules/#google-cloud/common/src/util.js:187:32)
at Object.handleResp (/user_code/node_modules/firebase-admin/node_modules/#google-cloud/common/src/util.js:131:18)
at /user_code/node_modules/firebase-admin/node_modules/#google-cloud/common/src/util.js:496:12
at Request.onResponse [as _callback] (/user_code/node_modules/firebase-admin/node_modules/#google-cloud/common/node_modules/retry-request/index.js:198:7)
at Request.self.callback (/user_code/node_modules/firebase-admin/node_modules/request/request.js:185:22)
at emitTwo (events.js:106:13)
at Request.emit (events.js:191:7)
at Request.<anonymous> (/user_code/node_modules/firebase-admin/node_modules/request/request.js:1161:10)
at emitOne (events.js:96:13)
at Request.emit (events.js:188:7)
code: undefined,
errors: undefined,
response: undefined,
message: 'Error during request.' }
(old question removed)
"Error: Can't set headers after they are sent" means that you tried to send two responses to the client. This isn't valid - you can send only one response.
Your code is clearly sending two 200 type responses to the client in the event that imageRef.delete() fails and the catch callback on it is triggered.
Also, you're mixing up await with then/catch. They're not meant to be used together. You choose one or the other. Typically, if you're using await for async programming, you don't also use then/catch with the same promise. This is more idiomatic use of await with error handling:
try {
await imageRef.delete()
res.status(200).send({status: 200, message: `Thank you for id ${id}`});
} catch (error) {
res.status(500).send({message: `Image deletion failed: ${err}`});
}
Note also that you typically send a 500 response to the client on failure, not 200, which indicates success.

Strange error on download using googleapis 7.1.0

I am downloading a file from Google Drive using nodejs module googleapis 7.1.0. When I do authentication or retrieve metadata, everything goes fine.
When the download is finished and the application is supposed to end, I get two different outcomes, both seem to be wrong.
In Windows, the program just hangs indefinitely and produces no output, no exception. I just hangs.
On FreeBSD, I get the following stack trace:
buffer.js:377
throw new Error('toString failed');
^
Error: toString failed
at Buffer.toString (buffer.js:377:11)
at BufferList.toString (/usr/home/jvavruska/gdrive/node_modules/googleapis/node_modules/google-auth-library/node_modules/request/node_modules/bl/bl.js:166:33)
at Request.<anonymous> (/usr/home/jvavruska/gdrive/node_modules/googleapis/node_modules/google-auth-library/node_modules/request/request.js:1035:36)
at emitOne (events.js:82:20)
at Request.emit (events.js:169:7)
at IncomingMessage.<anonymous> (/usr/home/jvavruska/gdrive/node_modules/googleapis/node_modules/google-auth-library/node_modules/request/request.js:1003:12)
at emitNone (events.js:72:20)
at IncomingMessage.emit (events.js:166:7)
at endReadableNT (_stream_readable.js:913:12)
at nextTickCallbackWith2Args (node.js:442:9)
I use node 4.4.5 on both machines (Windows 10 and FreeBSD 10) and the same version of googleapis (7.1.0).
The final function that does the download is here. name is read from file metadat using get API, auth is the auth object created from google.auth.OAuth2, googleDrive is proxy of google.drive('v3') and google is from google = require('googleapis') :
function googleDownload ( name, fileId, auth, downloadDirectory ) {
downloadDirectory = downloadDirectory || 'c:\\playground' ;
var targetFileName = path.join( downloadDirectory, name );
var dest = fs.createWriteStream(targetFileName);
console.log(`Starting download of ${fileId} as ${name}`);
googleDrive.files.get({
fileId: fileId,
auth: auth,
alt: 'media'
},
(err, response) => {
if(err) { console.log("Download error: ", err);}
else { console.log("Download completed."); }
})
.on('end', () => { console.log('All data received.'); })
.on('finish', () => { console.log('All data written.'); })
.on('close', () => { console.log('Connectin closed.'); })
.on('error', (err) => { console.log('Error during download: ', err); })
.pipe(dest);
}
By looking into the code I was not able to find a direct link between the place where the error is thrown and what was actually supposed to happen. I just noticed that the googleapis bundle seems to duplicate a number of methods or functions available in NodeJS API but cannot say what is the impact on the error.

Error using Redis Multi with nodejs

I am using Redis and consulting it from nodejs, using the module Redis.
When i exec a client.multi() and the redis server is down the callback doesn't send the error and the nodejs app terminates.
This is the error
/Users/a/db/node_modules/redis/index.js:151
throw callback_err;
^
TypeError: Cannot read property 'length' of undefined
at Command.callback (/Users/a/db/node_modules/redis/index.js:1098:35)
at RedisClient.flush_and_error (/Users/a/db/node_modules/redis/index.js:148:29)
at RedisClient.on_error (/Users/a/db/node_modules/redis/index.js:184:10)
at Socket.<anonymous> (/Users/a/db/node_modules/redis/index.js:95:14)
at Socket.EventEmitter.emit (events.js:95:17)
at net.js:441:14
at process._tickCallback (node.js:415:13)
this is my code:
Constructor class
var redis = require('redis');
var client;
function Redis(){
client = redis.createClient();
client.on("error", function (err) {
console.log("Error " + err);
});
}
Redis.prototype.multi = function(commands,callback){
var err = null;
client.multi(commands).exec(function (error, res) {
if(error){
process.nextTick(function(){
callback(error,null)
})
}else{
process.nextTick(function(){
callback(null,res)
})
}
});
}
FYI, I ran across this in an old lib that depended on old version of node_redis.
This issue was a bug and was fixed in v0.9.1 - November 23, 2013: https://github.com/mranney/node_redis/pull/457
I think that people are still reaching here... (not sure if this answers this specific question directly, but I assume people reaching here since the multi.exec() returns true / the event loop is not waiting for it's response.
After the fixes that went in (in node-redis), it is possible to wrap the result of exec with Promise, and then you will be sure that the result will include the replies from the multi.
So, you can add some redis commands to the multi:
await multi.exists(key);
await multi.sadd(key2,member);
And then in the result do something like:
return new Promise((resolve, reject) => {
multi.exec((err, replies) => {
if (err) {
reject(err);
}
return resolve(replies);
});
});
Otherwise, if you will just do: const reply = await multi.exec();
it will just return you true, and not the replies
** Important to mention - this refers to 'async-redis' and 'node-redis'

Resources