UnhandledPromiseRejectionWarning when adding a new record to DB - node.js

I am trying to add new records to my MongoDB database. The records come in the form of an array, and then, for each record, I add it as a new item to MongoDB.
The problem is that every time I try to add some records (30-50 records) I get this message:
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()
followed by this one:
UnhandledPromiseRejectionWarning: Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
This is the snippet I use for adding records:
// update the blacklist with a new entry
router.post("/updateBlacklist", async (req, res) => {
req.body.product_id.forEach(async element => {
const blacklistItem = new Blacklist({ product_id: element });
try {
const savedItem = await blacklistItem.save();
res.json(savedItem);
} catch (err) {
console.log(err);
res.json({ message: err });
}
});
});
The same warning appears also when I try to download some photos using this function:
router.post("/product_ids", async (req, res) => {
await downloadPhotos(req.body.items_id);
});
the function I call inside this route:
const axios = require("axios");
const download = require("./utils/download");
const downloadPhotos = async itemsIDs => {
try {
console.log(itemsIDs.length);
await itemsIDs.forEach(ID => {
axios
.get(
`https://${process.env.SHOPIFY_API_KEY}:${
process.env.SHOPIFY_PASSWORD
}#blablabla.myshopify.com/admin/products/${ID}/images.json?fields=id, src`
)
.then((req, res) => {
console.log(req.data.images[0].src);
download(req.data.images[0].src, req.data.images[0].id, 3000);
});
});
} catch (err) {
console.log(err);
}
};
and the download function:
const Fs = require("fs");
const Path = require("path");
const Axios = require("axios");
async function downloadImage(url, filename, timeout) {
const appDir = Path.dirname(require.main.filename);
const path = Path.resolve(appDir, "images", `${filename}.jpg`);
const writer = Fs.createWriteStream(path);
const response = await Axios({
url,
method: "GET",
responseType: "stream",
timeout: timeout
});
response.data.pipe(writer);
return new Promise((resolve, reject) => {
writer.on("finish", resolve);
writer.on("error", reject);
});
}
module.exports = downloadImage;
What am I doing wrong in this approach?

This error:
UnhandledPromiseRejectionWarning: Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
It's because of these lines:
res.json(savedItem);
res.json({ message: err });
You're running these lines several times, because they are inside the loop, you can't respond a request more than once because the connection it's closed after the first call.
Try with this:
router.post("/updateBlacklist", async (req, res) => {
const savedList = [];
req.body.product_id.forEach(async element => {
const blacklistItem = new Blacklist({ product_id: element });
try {
const savedItem = await blacklistItem.save();
savedList.push(savedItem)
} catch (err) {
console.log(err);
savedList.push({ message: err });
}
});
res.json(savedItem);
});
UPDATE (suggestion):
router.post("/updateBlacklist", async (req, res) => {
const items = req.body.product_id.map(element =>
new Blacklist({ product_id: element })
});
const result = await Blacklist.insertMany(items); // not sure about the syntax
res.json(result);
});

Related

Cannot conect to redis in Node

I'm trying to create a basic caching app just to test redis. Im using Redis Version: 4.0.6.
First I was getting error clientclosederror: the client is closed.
Then, after reading the docs, I added
let client;
(async ()=> {
client = redis.createClient()
await client.connect()
})();
But now, when trying on Postman, it just hangs, no response is returned
Full Code:
const express = require("express");
const redis = require("redis");
const axios = require('axios')
const app = express();
let client;
(async ()=> {
client = redis.createClient()
await client.connect()
})();
app.get('/result', async (req, res) => {
const searchTerm = req.query.name;
try {
await client.get(searchTerm, async (err, result) => {
console.log('cached called')
if (err) throw err;
if (result) {
res.status(200).send({
result: JSON.parse(result),
message: "data retrieved from the cache"
});
}
else {
const result = await axios.get(`https://api.agify.io/?name=${searchTerm}`);
await client.set(searchTerm, JSON.stringify(result.data));
return res.status(200).send({
result: result.data,
message: "cache miss"
});
}
})
} catch (error) {
console.log('get error', error)
return res.status(500).send({ message: error.message })
}
})
app.listen(process.env.PORT || 3000, () => {
console.log("Node server started");
});
client.get doesn't need a callback function. It's async. My guess is that it's never getting called and thus Express is not returning anything.
Try this instead:
const result = await client.get('foo')
if (result !== null) {
// it's a hit
} else {
// it's a miss
}

Cannot use Async/Await in a Serverless NodeJS Function

I'm new to Serverless with NodeJS, I'm trying to use an Async function to hash a password and perform some stuff in a database, the problem is that when I declare the functions async I always get this error:
But if I remove the async keyword here:
module.exports.login = async (event, context, callback) => {
the function runs properly, but of course I won't be able to use promises within the function.
This is an endpoint, to make API calls.
Here's my code:
'use strict';
require('dotenv').config({ path: './.env' });
const dataBase = require('./utils/db');
const bcrypt = require('bcrypt');
const encryptPassword = async (plainPassword) => {
const saltRounds = 10;
const hashedPassword = await bcrypt.hash(plainPassword, saltRounds);
return hashedPassword;
};
module.exports.login = async (event, context, callback) => {
context.callbackWaitsForEmptyEventLoop = false;
const parsedBody = JSON.parse(event.body);
const connect = dataBase.connectToDatabase();
const newPassword = await encryptPassword('test');
console.log(newPassword);
connect.query('SELECT * FROM users', (error, results, fields) => {
if (error) {
console.error(error);
callback(null, {
statusCode: 500,
body: JSON.stringify({
error: JSON.stringify(error),
message: 'Internal Server Error Booh!',
}),
});
}
if (results) {
callback(null, {
statusCode: 200,
body: JSON.stringify({
error: results,
message: 'No Error',
}),
});
}
});
};
Here my db connection and config:
db.js
const mysql = require('mysql');
const isDev = true;
// Create mySQL Connection
const connectToDatabase = () => {
const pool = mysql.createPool({
host: isDev ? process.env.DATABASE_HOST_DEV : process.env.DATABASE_HOST_PROD,
user: isDev ? process.env.DATABASE_USER_DEV : DATABASE_USER_PROD,
password: isDev ? process.env.DATABASE_PASSWORD_DEV : DATABASE_PASSWORD_PROD,
database: isDev ? process.env.DATABASE_DATABASE_DEV : DATABASE_DATABASE_PROD,
multipleStatements: true,
});
return pool;
};
exports.connectToDatabase = connectToDatabase;
exports.mysql = mysql;
What am I missing?
EDIT:
The purpose of all this, it's because I'm learning serverless.
All this will end up in an AWS Lambda with an endpoint using API Gateway.
So, when you call this endpoint, you will be sending some params to register/login into an app.
I need to use async/await because if you register an account, the password needs to be hashed and then store into a database or if you login, the password will need to be compared, both of these actions are asynchronous ones.
That's why I need the endpoint to be an async function.
EDIT 2:
Reading this post: https://github.com/netlify/netlify-dev-plugin/issues/160
As Phil mention, async and callback shouldn't be used togehter, so I modified my code like this, and same error:
'use strict';
require('dotenv').config({ path: './.env' });
const dataBase = require('./utils/db');
const bcrypt = require('bcrypt');
const encryptPassword = async (plainPassword) => {
const saltRounds = 10;
const hashedPassword = await bcrypt.hash(plainPassword, saltRounds);
return hashedPassword;
};
module.exports.login = async (event, context, callback) => {
context.callbackWaitsForEmptyEventLoop = false;
const parsedBody = JSON.parse(event.body);
const connect = dataBase.connectToDatabase();
const newPassword = await encryptPassword('test');
console.log(newPassword);
connect.query('SELECT * FROM users', (error, results, fields) => {
if (error) {
console.error(error);
return {
error: 'some error to test',
};
}
if (results) {
return {
body: 'someBody',
};
}
});
};
FINAL FUNCTIONAL CODE:
In case any wonder how to make these queries as promises, here's my approach, hope it helps anyone out there struggling with the same.
'use strict';
require('dotenv').config({ path: './.env' });
const dataBase = require('./utils/db');
const bcrypt = require('bcrypt');
const encryptPassword = async (plainPassword) => {
const saltRounds = 10;
const hashedPassword = await bcrypt.hash(plainPassword, saltRounds);
return hashedPassword;
};
module.exports.login = async (event, context, callback) => {
context.callbackWaitsForEmptyEventLoop = false;
const parsedBody = JSON.parse(event.body);
const connect = dataBase.connectToDatabase();
const hashedPassword = await encryptPassword('test');
console.log(hashedPassword);
return new Promise((resolve, reject) => {
connect.query('SELECT * FROM users', (error, result) => {
if (error) {
reject(
callback(null, {
statusCode: 500,
body: JSON.stringify({
error: JSON.stringify(error),
message: 'Internal Server Error Booh!',
}),
})
);
}
if (result) {
resolve(
callback(null, {
statusCode: 200,
body: JSON.stringify({
data: result,
message: 'No Error',
}),
})
);
}
});
});
};
Regards.
Taking a quick look at the docs, it doesn't look like you should be mixing async with the callback arg. Just use one or the other, not both.
You could migrate to the mysql2 library so you can use promises or stick with the one you've got and use encryptPassword("test").then() instead of await
// no async
module.exports.login = (event, context, callback) => {
// remove this, you don't want it
// context.callbackWaitsForEmptyEventLoop = false;
const parsedBody = JSON.parse(event.body);
const connect = dataBase.connectToDatabase();
encryptPassword("test").then(newPassword => { // use .then()
console.log(newPassword);
// callback APIs are a pain, recommend migrating to mysql2 and use promises
connect.query('SELECT * FROM users', (error, results, fields) => {
if (error) {
console.error(error);
return callback(null, {
statusCode: 500,
body: JSON.stringify({
error: JSON.stringify(error),
message: 'Internal Server Error Booh!',
}),
});
}
callback(null, {
statusCode: 200,
body: JSON.stringify({
error: results, // error?
message: 'No Error',
}),
});
}
});
}

How to mock function using node-tap on fastify inject

I want to make 100% coverage on this function with node-tap but I can't mock the error part, it always throw
Cannot find module 'create' Require stack: - /home/mptr8/Code/Projects/me/fastify-example/fastify-postgres/test/integration.js
But I have create function on my query.js file, what do I do wrong here? Why it doesn't invoke the method?
t.mock("../query.js", {
create: () => {
throw new Error();
},
});
I also try this combination, because query.js are dependent on db.js. Now the mock error gone but still I'm not getting the error throw from my fastify.inject.
t.mock("../db.js", {
"../query.js": {
create: () => { throw new Error() },
},
});
app.post("/", async (request, reply) => {
try {
const { body } = request;
const book = create(body.title);
reply.send(book);
} catch (error) {
// this part are not covered
reply.code(500).send({ message: "internal server error" });
}
});
here are my complete code. You can see the full code on this github repository.
// server.js
const fastify = require("fastify");
const {
migration,
create,
} = require("./query");
const db = require("./db");
function build(opts = {}) {
const app = fastify(opts);
migration();
app.post("/", async (request, reply) => {
try {
const { body } = request;
const book = create(body.title);
reply.send(book);
} catch (error) {
reply.code(500).send({ message: "internal server error" });
}
});
app.addHook("onClose", (_instance, done) => {
db.close();
done();
});
}
module.exports = build;
// db.js
const { Pool } = require("pg");
const pool = new Pool({
connectionString:
"postgresql://postgres:postgres#localhost:5432/fastify_postgres?schema=public",
});
module.exports = {
query: (text, params) => pool.query(text, params),
close: () => pool.end(),
};
// query.js
const db = require("./db");
async function migration() {
await db.query(`
CREATE TABLE IF NOT EXISTS books (
id serial PRIMARY KEY,
title varchar (255) NOT NULL
)
`);
}
async function create(title) {
return await db.query("INSERT INTO books (title) VALUES ($1)", [title]);
}
module.exports = { migration, create };
// test.js
const tap = require("tap");
const fastify = require("../server");
tap.test("coba", async (t) => {
const app = await fastify();
t.test("should success create books", async (t) => {
const response = await app.inject({
method: "POST",
url: "/",
payload: {
title: "Hello,World!",
},
});
t.equal(response.statusCode, 200);
});
t.test("should throw error", async (t) => {
const app = await fastify();
// it doesn't throw the error :((
t.mock("../query.js", {
create: () => {
throw new Error();
},
});
const response = await app.inject({
method: "POST",
url: "/",
payload: {
title: "Hello,World!",
},
});
t.equal(response.statusCode, 500);
// call app close on last test child to close app and db properly
app.close();
});
});
You should use the returned value by the t.mock function:
const build = t.mock({
"../server": {
"./query.js": {
create: () => { throw new Error() },
}
}
})
const app = await build({})

I am getting UnhandledPromiseRejectionWarning error when trying to connect node js with mongoDB Atlas cloud

I created a app.js file and there I am trying to connect with mongoDB atlas. The error '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()' is throwing when I run in terminal.
const connect = async function () {
const MongoClient = require('mongodb').MongoClient;
const uri = "mymongoDB atals url for nodejs";
MongoClient.connect(uri, { useNewUrlParser: true });
const collection = client.db("feedback").collection("itinerary");
// perform actions on the collection object
client.close();
};
connect().then(() => {
console.log('handle success here');
}).catch((exception) => {
console.log('handle error here: ', exception)
})
Try putting the async function operations in try catch block as below. I hope this should do the work.
const connect = async function () {
try {
const MongoClient = require('mongodb').MongoClient;
const uri = "mymongoDB atals url for nodejs";
MongoClient.connect(uri, { useNewUrlParser: true });
const collection = client.db("feedback").collection("itinerary");
// perform actions on the collection object
client.close();
} catch (e) {
console.log("Error", e)
}
};
connect().then(() => {
console.log('handle success here');
}).catch((exception) => {
console.log('handle error here: ', exception)
})
Try this:
const MongoClient = require('mongodb').MongoClient;
const connect = function () {
return new Promise((resolve, reject) => {
try {
const uri = "mymongoDB atals url for nodejs";
const client = new MongoClient(uri, { useNewUrlParser: true });
client.connect(err => {
if (err) {
reject(err)
}
const collection = client.db("feedback").collection("itinerary");
client.close();
resolve();
});
} catch (e) {
reject(e);
}
})
};
connect().then(() => {
console.log('handle success here');
}).catch((exception) => {
console.log('handle error here: ', exception)
})
Try this approach:
const MongoClient = require('mongodb').MongoClient;
// replace the uri string with your connection string.
const uri = "mymongoDB atals url for nodejs"
MongoClient.connect(uri, function(err, client) {
if(err) {
console.log('handle error here: ');
}
console.log('handle success here');
const collection = client.db("feedback").collection("itinerary");
// perform actions on the collection object
client.close();
});
Try by wrapping all the content of your function in a try/catch block:
const connect = async function () {
try {
const MongoClient = require('mongodb').MongoClient;
const uri = "mymongoDB atals url for nodejs";
MongoClient.connect(uri, { useNewUrlParser: true });
// most probably this is throwing the error. Notice the extra await
const collection = await client.db("feedback").collection("itinerary");
// perform actions on the collection object
client.close();
} catch (e) {
console.log(`Caught error`,e)
}
};
connect().then(() => {
console.log('handle success here');
}).catch((exception) => {
console.log('handle error here: ', exception)
})

Jest/SuperTest Express integration tests - Can't set headers after they are sent. (when you call the same endpoint in multiple tests)

This one's killing me..
I'm writing integration tests for an Express (Typescript) app, using Jest and Supertest.
I have multiple tests for the same endpoint, to test responses from when a mocked service returns data correctly and when it rejects a promise with an Error object.
The tests run fine when each request() in each it() block hits a unique end point, but when endpoints are shared between blocks I get the following error:
Can't set headers after they are sent.
This is my test code:
let request = null;
let app = null;
const async = require('async');
import GError from '../../src/services/ErrorService';
const { list } = require('../../src/controllers/RecipeController');
let throwError: boolean = false;
let error = null;
const errorMsg: string = 'Something went wrong!';
const listData: Array<object> = [{id: 1, base: 'something'}];
jest.mock('../../src/services/RecipeService', () => {
return jest.fn().mockImplementation(() => ({
list: jest.fn(() => {
if (throwError) {
return Promise.reject(error);
}
return Promise.resolve(listData);
})
}));
});
beforeEach(() => {
request = require('supertest');
app = require('../../src/app');
});
afterEach( ( done ) => {
throwError = false;
error = null;
app.close( () => {
delete require.cache[require.resolve('../../src/app')];
done();
});
});
describe('Root Path', () => {
it('should return a welcome message', (done) => {
request(app)
.get('/')
.end((err, res) => {
expect(res.text).toEqual('Test API.');
expect(res.statusCode).toBe(200);
done();
});
});
});
describe('Recipe List', () => {
it('should call controller and return correct response when successful or error is thrown in service', (done) => {
const path: string = '/recipes/list';
request(app)
.get(path)
.end((err, res) => {
expect(JSON.parse(res.text)).toEqual({
recipes: listData
});
done();
});
});
it('should return an error response if service rejects promise', (done) => {
throwError = true;
error = new GError(errorMsg);
const path: string = '/recipes/list';
request(app)
.get(path)
.end((err, res) => {
expect(JSON.parse(res.text)).toEqual({
errors: {
message: errorMsg
}
});
done();
});
});
});
I think I need to reset the app in between tests, which is what I'm trying to achieve with:
beforeEach(() => {
request = require('supertest');
app = require('../../src/app');
});
But with no joy. Can anyone shine a light?
UPDATE:
Here's the controller method the route hits:
exports.list = async (req, res, next) => {
const recipes: IRecipeList = await recipeService.list().catch(err => {
return next(err);
});
const response: IRecipeListResponse = {recipes};
res.status(200).json(response);
};
SOLVED:
So it turned out to be nothing to do with Jest / Superagent (I was sure it was to do with one of these). Strangely though I only get this error in the context of running integration tests, there is no error when hitting the end point in Postman - which was super confusing.
PROBLEM:
exports.list = async (req, res, next) => {
const recipes: IRecipeList = await recipeService.list().catch(err => { . //this doesn't stop the execution past this await
return next(err);
});
//this still gets processed if the service rejects the promise
const response: IRecipeListResponse = {recipes};
res.status(200).json(response);
};
SOLUTION:
exports.list = async (req, res, next) => {
let error = false;
const recipes: IRecipeList = await recipeService.list().catch(err => {
error = true;
return next(err);
});
if (error) {
return;
}
const response: IRecipeListResponse = {recipes};
return res.status(200).json(response);
};
This error occurs when you send response more than once.

Resources