I'm a beginner in both Stackoverflow and NodeJS/Mongoose, I'm sorry if I have an error or break a rule. Thank you in advance.
I need a function that it return all the nearby products there are in my location, this is given through the user which "id" is a request called "user".
I try making this function, where finalProducts return all the products that they exit at the search, but when I try to add as component of result body finalProducts return data empty. The error is the following:
throw er; // Unhandled 'error' event ^
Error: Can't set headers after they are sent. at ServerResponse.setHeader (_http_outgoing.js:371:11) at ServerResponse.header (/home/frangaliana/Escritorio/client-thingy/node_modules/express/lib/response.js:730:10) at ServerResponse.send (/home/frangaliana/Escritorio/client-thingy/node_modules/express/lib/response.js:170:12) at ServerResponse.json (/home/frangaliana/Escritorio/client-thingy/node_modules/express/lib/response.js:256:15) at ServerResponse.send (/home/frangaliana/Escritorio/client-thingy/node_modules/express/lib/response.js:158:21) at /home/frangaliana/Escritorio/client-thingy/controllers/product.js:200:41 at /home/frangaliana/Escritorio/client-thingy/node_modules/mongoose/lib/query.js:2916:18 at newTickHandler (/home/frangaliana/Escritorio/client-thingy/node_modules/mpromise/lib/promise.js:234:18) at _combinedTickCallback (internal/process/next_tick.js:73:7) at process._tickCallback (internal/process/next_tick.js:104:9)
I show the code and the models for help to understand the trouble:
Function that search nearby products in controller product.js:
function getNearbyProducts(req, res) {
let userId = req.user;
let point;
var geoOptions = {
spherical: true,
maxDistance: 500
}
User.findById(userId, {password:0})
.populate('location','coordinates')
.exec(function (err, result) {
if (err) console.log('No se ha podido encontrar la localización')
point = {
type: "Point",
coordinates: [parseFloat(result.location.coordinates[0]),parseFloat(result.location.coordinates[1])]
}
Location.geoNear(point,geoOptions, function(err, resultLocations) {
for(var i = resultLocations.length - 1 ; i >= 0 ; i--){
var nearLocation = resultLocations[i].obj.id
var queryUser = {"location": nearLocation}
User.find(queryUser)
.exec(function (err, resultUsers) {
for(var j = resultUsers.length - 1 ; j >= 0; j--) {
if(resultUsers[j] !== undefined){
var exactUser = resultUsers[j].id
var limit;
if(req.query.limit) {
limit = parseInt(req.query.limit)
if(isNaN(limit)){
return next(new Error())
}
} else {
limit = 10;
}
var queryProduct = {"user": exactUser}
if(req.query.before) {
queryProduct = {"user": exactUser, "_id" : {$lt: req.query.before}};
}else if (req.query.after) {
queryProduct = {"user": exactUser, "_id" : {$gt: req.query.after}};
}
Product.find(queryProduct)
.limit(limit)
.populate('user')
.exec(function (err, resultProducts) {
var finalProducts = [];
for(var k = resultProducts.length - 1 ; k >= 0; k--){
if(resultProducts[k] !== undefined){
finalProducts.push(resultProducts[k])
}
}
if(finalProducts.length > 0){
if(req.query.before){
products.reverse();
}
var finalResult = {
data: finalProducts,
paging: {
cursors: {
before: finalProducts[0].id,
after: finalProducts[finalProducts.length-1].id
},
previous: 'localhost:3000/api/products?before='+finalProducts[0].id,
next: 'localhost:3000/api/products?after='+finalProducts[finalProducts.length-1].id,
},
links: {
self: 'localhost:3000/api/products',
users: 'localhost:3000/api/users'
}
}
} else {
var finalResult = {
data: finalProducts,
paging: {
cursors: {
before:undefined,
after:undefined
},
previous: undefined,
next: undefined
},
links: {
self: 'localhost:3000/api/products',
users: 'localhost:3000/api/users'
}
}
}
res.status(200).send(finalResult);
})
}
}
})
}
})
})
})
Models:
user.js
'use strict';
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const bcrypt = require('bcrypt-nodejs');
const Location = require('../models/location');
const crypto = require('crypto');
const UserSchema = new Schema({
email: {
type: String,
lowercase: true,
//Añadir campo unique: true para que sólo se pueda registrar un email
},
name: String,
password: String,
userimg: String,
gender: Boolean,
birthdate: Date,
signUpDate: {
type: Date,
default: Date.now(),
},
location:{
type: Schema.ObjectId,
ref: 'Location'
}
});
UserSchema.pre('save', function(next) {
let user = this;
if (!user.isModified('password')) return next();
bcrypt.genSalt(10, (err, salt) => {
if (err) return next(err);
bcrypt.hash(user.password, salt, null, (err, hash) => {
if (err) return next(err);
user.password = hash;
next();
});
});
});
UserSchema.methods.gravatar = function() {
if(!this.email) return `https://gravatar.com/avatar/?s=200&d=retro`
const md5 = crypto.createHash('md5').update(this.email).digest('hex')
return `https://gravatar.com/avatar/${md5}?s=200&d=retro`
}
module.exports = mongoose.model('User', UserSchema);
product.js
'use strict'
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const User = require('../models/user');
var max = [5 , 'The value of ({VALUE}) exceeds the limit ({MAX}). ']
var min = [1 , 'The value of ({VALUE}) is beneath the limit ({MIN}). ']
const ProductSchema = Schema({
title: String,
price: {
type: Number,
default: 0
},
user: {
type: Schema.ObjectId,
ref: 'User'
},
categoryproduct: {
type: String,
enum:['Moda y Accesorios', 'Motor', 'Electrónica', 'Deporte', 'Libros, Música y Películas', 'Electrodomésticos', 'Servicios', 'Muebles y Decoración', 'Otros'],
default: 'Electrónica'
},
description: {
type: String,
default: 'Objeto para vender'
},
visits: {
type: Number,
default: 0
},
status: {
type: Boolean,
default: false
},
publicationdate: {
type: Date,
default: Date.now()
},
salesrating: {
type: Number,
max: max,
min: min,
default: 1
},
salescomment: {
type: String,
default: 'Perfecto'
}
})
module.exports = mongoose.model('Product', ProductSchema);
location.js
'use strict';
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const LocationSchema = new Schema({
type: {
type: String,
default: "Point"
},
coordinates: {
type: [Number],
index: "2dsphere",
default: [38.280153, -0.712901]
}
})
module.exports = mongoose.model('Location', LocationSchema);
I hope this question can be resolved or at least someone explain me because it doesn't work well. A lot of thanks again!
EDIT: (Because I have fix the problem)
Thanks to skirtle who gave me the idea to solve this.
I didn't control the asynchronous calls that threw the searches with Mongoose and that generated multiple responses, so as he told me I started using Promises to keep track of them when the result was throwing me an array of id's whether they were from User, Location or Product I treated them one by one.
I recalled that a Mongoose query could be accompanied by a filter {$in:[array]}
that returned all results containing any of these id's (in my case) that had the array looking like this:
function getNearbyProducts(req, res) {
var userId = req.user;
var promiseUser = User.findById(userId, {password: 0})
.populate('location')
.exec()
promiseUser
.then(function(result){
return result.location;
})
.then( function(resultUser){
return Location.geoNear(
{type:'Point', coordinates: [parseFloat(resultUser.coordinates[0]),parseFloat(resultUser.coordinates[1])]},
{maxDistance:100000, spherical: true}
).then(function(locsGeoNear){
var resultGeoNear = []
for(var i = locsGeoNear.length - 1; i >= 0; i--){
if(resultUser.id != locsGeoNear[i].obj.id){
resultGeoNear.push(locsGeoNear[i].obj.id)
}
}
return resultGeoNear
})
})
.then(function(resultSearchLocs){
var queryUsersByLocation = {'location': {$in: resultSearchLocs}}
return User.find(queryUsersByLocation, {password: 0})
.exec()
.then(function(usersSearchs){
var resultUsers = []
for(var i = usersSearchs.length - 1; i >= 0; i--){
if(userId != usersSearchs[i].id){
resultUsers.push(usersSearchs[i].id)
}
}
return resultUsers
})
})
.then(function(resultSearchUsers){
var limit;
if(req.query.limit) {
limit = parseInt(req.query.limit)
if(isNaN(limit)){
return next(new Error())
}
} else {
limit = 10;
}
var queryProductsByUsers = {'user': {$in: resultSearchUsers}}
//Para obtener la página anterior a un id
if (req.query.before) {
queryProductsByUsers = {'user': {$in: resultSearchUsers}, "_id" : {$lt: req.query.before}};
//Para obtener la página posterior a un id
} else if (req.query.after) {
queryProductsByUsers = {'user': {$in: resultSearchUsers}, "_id": {$gt: req.query.after}};
}
return Product.find(queryProductsByUsers)
.limit(limit)
.exec()
})
.then(function(resultSearchProducts){
if(resultSearchProducts.length > 0){
if(req.query.before){
resultSearchProducts.reverse();
}
var resultFinal = {
data: resultSearchProducts,
paging: {
cursors: {
before: resultSearchProducts[0].id,
after: resultSearchProducts[resultSearchProducts.length-1].id
},
previous: 'localhost:3000/api/products?before='+resultSearchProducts[0].id,
next: 'localhost:3000/api/products?after='+resultSearchProducts[resultSearchProducts.length-1].id,
},
links: {
self: 'localhost:3000/api/products',
users: 'localhost:3000/api/users'
}
}
} else {
var resultFinal = {
data: resultSearchProducts,
paging: {
cursors: {
before:undefined,
after:undefined
},
previous: undefined,
next: undefined
},
links: {
self: 'localhost:3000/api/products',
users: 'localhost:3000/api/users'
}
}
}
res.setHeader('Content-Type', 'application/json');
res.status(200).send(resultFinal);
})
.catch(function(err){
console.log(`${err}`)
})
}
Many thanks to the community but above all to skirtle who gave me the keys to reach my solution.
Greetings!
If you add the following logging before you call send
:
console.log('sending response');
res.status(200).send(finalResult);
I believe you'll find that you're calling send
multiple times on the same request, which isn't allowed. When you call send
the first time the request/response is over and any attempt to send more data will result in an error.
I'm struggling to follow the code but I believe the cause is all that looping you're doing. You need to wait until all your DB queries are done and you've gathered your final data before you call send
.
You may find Promises a useful way to reduce the complexity in products.js
but even if you don't fancy using them I highly recommend a bit of refactoring to make that file intelligible. As a general rule the Pyramid of Doom is a sign that you've got problems https://en.wikipedia.org/wiki/Pyramid_of_doom_(programming)