How to change the default error output in restify - node.js

Is there any way that I can change the default error output? Say I'm going to change the rest error output:
{
"code": "InvalidArgumentError",
"message": "blah blah..."
}
to:
{
"code": 10001,
"message": "blah blah",
"extraMsg": "blah blah"
}
Here are some of my ideas:
Listen to the error events.
It seems like not all the RestError have emitted extra events (like NotFound, MethodNotAllowed, VersionNotAllowed... do). So I can't catch all the errors to rewrite them.
Listen to an event before response data sent.
I look through the official documents and have found nothing relative.
Modify the implementation of the RestError class.
Well it's obviously not a good approach.
Any other ideas?

Finally I provide a customized JSON formatter to get what I want:
var server = restify.createServer( {
formatters: {
'application/json': function customizedFormatJSON( req, res, body ) {
// Copied from restify/lib/formatters/json.js
if ( body instanceof Error ) {
// snoop for RestError or HttpError, but don't rely on
// instanceof
res.statusCode = body.statusCode || 500;
if ( body.body ) {
body = {
code: 10001,
scode: body.body.code,
msg: body.body.message
};
} else {
body = {
code: 10001,
msg: body.message
};
}
} else if ( Buffer.isBuffer( body ) ) {
body = body.toString( 'base64' );
}
var data = JSON.stringify( body );
res.setHeader( 'Content-Length', Buffer.byteLength( data ) );
return data;
}
}
} );

While the answers above might work, the easiest way to add a custom field to the error body is to call the restify error constructor with an object (hash) instead of a string. The object has to contain the body key which is what you will see in the browser.
For example:
return next(new restify.InvalidArgumentError({body: {field: 'password', message: 'Password has to be at least 6 characters long'}}));
or
return next(new restify.UnauthorizedError({body: {foo: 'bar', name: 'john doe', message: 'whatever error message'}}));

Restify offer many ways to implement error management : http://mcavage.github.io/node-restify/#Error-handling
Why don't you create a new error type "myError" just like sample code :
var restify = require('restify');
var util = require('util');
function MyError(message) {
restify.RestError.call(this, {
restCode : 'MyError',
statusCode : 418,
message : message,
constructorOpt: MyError
});
this.name = 'MyError';
}
util.inherits(MyError, restify.RestError);
For common errors I think that overloading methods is not such a bad idea... (I don't speak about modifying restify, just overloading functions using prototype)
(edited)

I was able to provide additional data adding a property to the body object.
Notice the this.body.errors = errors line
var restify = require('restify');
var util = require('util');
function ValidationError(message, errors) {
restify.RestError.call(this, {
restCode: 'ValidationError',
statusCode: 400,
message: message,
constructorOpt: ValidationError
});
this.name = 'ValidationError';
this.body.errors = errors; //<---
}
util.inherits(ValidationError, restify.RestError);
`

You can use restify-errors-options
Your example simply becomes:
const restify = require('restify');
const errors = require('restify-errors');
const errorsOptions = require('restify-errors-options');
errorsOptions.add('extraMsg');
const err = new errors.BadRequestError({extraMsg: 'whatever you want'});
err.toJSON();
//=> {code: 'BadRequest', message: '', extraMsg: 'whatever you want'}
Please also note that the solution provided was only tested on restify 5.x
Follow this issue for more information.

Related

How to override error message in #hapi/joi?

I am trying to override custom error message in Joi.
let's say i have a schema like follow.
const joiSchema = Joi.object({
name: Joi.string().required(),
email: Joi.string().email().required()
})
try{
const schema = joiSchema.validateAsync(req.body);
}catch(error){
error.details.map((detail) => {
// customize error message
});
}
I need to send error message like follow.
{ errors: { name: "Name is Required.", email: "Email is Required." } }
How to get fieldName like name in details array in Validation Error.
I found a workaround like below.
const errors = [];
err.details.forEach((detail) => {
const currentMessage = detail.message;
detail.path.forEach((value) => {
errors.push({ [value]: currentMessage });
});
});
You can just get the error and process like it
catch(error){
var data = data.details;
var message = data[0].message; // you can get other fields also like this
var json={"status":"0","message":message,"data":{}}; // you can customize your json response
}
Following approach too help
const errorList = [...errors];
errors.forEach((error, index) => {
const tmpError = { ...error };
tmpError.message = "Your custom error message";
errorList[index] = tmpError;
});
return errorList;

Getting ERROR: uncaughtException: source.on is not a function, when using request and multiparty for multipart/form-data

I am trying to send data from my node application to a 3rd party HTTP endpoint.
I am parsing the data on the request object coming from the client using the multiparty module and sending the data through the request module. I am getting the error
error: uncaughtException: source.on is not a function
var request = require('request');
const multiparty = require('multiparty');
function addAttachment(req, res) {
let form = new multiparty.Form();
let parsedFile = {};
const formData = {};
form.parse(req, function(err, fields, files){
Object.keys(fields).forEach(function(name) {
formData[name] = fields[name][0];
});
Object.keys(files).forEach(function(name) {
logger.debug(name);
parsedFile[name] = files[name][0];
});
formData.uploadFile = parsedFile.uploadFile;
logger.debug('formData ', formData);
reqOptions.url = imageURL;
reqOptions.formData = formData;
logger.debug('REQ_OPTIONS ', reqOptions);
request.post(reqOptions, function (err, response, body) {
if (err) {
logger.warn(req, ' Error sending attachment', err);
res.status(400);
res.json({ "msg": "Error sending attachment" });
} else {
res.status(201);
logger.debug('BODY ', body);
res.send(body);
}
});
});
}
The reqOptions obj contains the headers, url, auth obj, we then add the form data to it.
When I log the form data it looks to be in the correct format
{
"meta": {
"prop1": "xxxxxx",
"prop2": "xxxxxxxxxxxxx",
"uploadFile": {
"fieldName": "uploadFile",
"originalFilename": "test.PNG",
"path": "/tmp/W1IppPiK04JpkPrnZWEhzkmV.PNG",
"headers": {
"content-disposition": "form-data; name=\"uploadFile\"; filename=\"test.PNG\"",
"content-type": "image/png"
},
"size": 42786
}
}
}
This error is probably one of the best examples how error message can be perfectly misleading. Therefore. it's very frustrating to do RCA of the issue:
ERROR: uncaught Exception: source.on is not a function
Actually there is nothing about any function here. In my case, I spent hours scratching my head and finally only to find it is JSON under another JSON which was causing this error:
let subJson =
{
field1: "value1",
field2: "value2"
}
let myJson =
{
field1: "value1",
field2: "value2",
field3: subJson
}
createFormData(myJson);
This is it! When you call createFormData with myJson as parameter, you will see exception source.on is not a function! And we keep thinking where is that function?
Solution is JSON.stringify
field3: JSON.stringify(subJson)
Will solve this issue.
javascript!
So after some hair pulling and digging around, I was able to post form data to the external API. I decide to change the node modules I was using to connect-multiparty. Connect will parse the request headers and decode the post-form-data allowing you to access the data from the req obj E.G req.body now have the added properties and req.files has uploaded files.
const multipart = require('connect-multiparty');
const multipartMiddleware = multipart();
Then add the multipartMiddleware to the route.
app.post('/api/addAttachment' multipartMiddleware, MyController.addAttachment);
Then in my controller file I changed the code to use connect-multipart.
const fs = require('fs');
var request = require('request');
function addAttachment(req, res) {
const TMP = '/tmp';
let formData = {};
Object.keys(req.body).forEach((propName) =>{
if (typeof propName === 'string') {
logger.debug(propName, ' is a string');
formData[propName] = req.body[propName];
} else {
logger.debug(propName, ' is not a string')
}
});
//The files get added to the tmp folder on the files system,
//So we create a stream to read from tmp folder,
//at the end end we need to delete the file
formData['uploadFile'] = fs.createReadStream(req.files.uploadFile.path);
logger.debug('FORM DATA ', formData, '\n');
reqOptions.url = imageUrl;
reqOptions.headers = {'Content-Type': 'multipart/form-data','Accept': 'application/json'};
reqOptions.formData = formData;
logger.debug('REQ_OPTIONS ', reqOptions, '\n');
request.post(reqOptions, function (err, response, body) {
if (err) {
removeFiles(TMP);
logger.warn(req, ' Error sending attachment', err);
res.status(400);
res.json({"msg": "Error sending attachment"});
} else {
removeFiles(TMP);
res.status(201);
logger.debug('BODY ', body);
res.send(body);
}
});
}

nodejs: How to nicely import constants?

I have one file with constant definitions (common.js):
var Errors = Object.freeze({
SUCCESS: {code: 0, message: 'Success'},
NOT_ENOUGH_ARGUMENTS: {code: 1, message: 'Not enough arguments provided'},
NOT_ALLOWED_ARGUMENT: {code: 2, message: 'Argument value is not allowed'}
});
module.exports = Errors;
And another file that uses this one (profile.js):
var Errors = require('../../common');
module.exports.profileCreate = function (req, res) {
var name = req.query.name;
var email = req.query.email;
if (!name || !email) {
res
.status(403)
.json({error: Errors.NOT_ENOUGH_ARGUMENTS});
return;
}
// ...
}
It looks to me that module.exports in first file and var Errors=require() in second one are having excessive syntax. Moreover I don't know what to do if I'd like to make some more enum constants instead of single Errors object.
What I have to do in order to use my enum object in other files of the project? Should I write down a bunch of exports for every enum object in the future, like:
module.exports.Errors = Object.freeze({ ... });
module.exports.Result = Object.freeze({ ... });
// ...etc

Sending HTTP Post request from node to Foxx service (ArangoDB)

I am trying to send a post request from a node + express server to my Foxx service on Arangodb.
On the node side :
var route = arangopi + '/edge/' + col.name ;
var body = {data: data, from: fromId, to: toId} ;
console.log('|| body :', route, body) ;
>> || body : http//XXX/_db/my-DB/my-foxx-service/path/to/visitedBy { data: { isBackup: true, text: '', isHint: true, continuance: 3441.5 }, from: 'Drop/27237133', to: 'Bot/41116378' }
return requestify.post (route, body)
On the Foxx side, I receive the request but the logs tell me it has no body :
router.post('/path/to/:param', function (req, res) {
console.log ('|| body :', req.body)
var data = req.body ;
var result = api.DoSomething (req.stateParams.param, data)
res.send(result)
})
.response(joi.object().required(), 'Entry stored in the collection.')
.summary('Summary')
.description('Description')
>> || body : [Object { "binarySlice" : function binarySlice() { [native code] }, "asciiSlice" : function asciiSlice() { [native code] }, "base64Slice" : function base64Slice() { [native code] }, "ucs2Slice" : function ucs2Slice() { [native code] }, "hexSlice" : f...
On the node side I also tried the 'request' module.
return request.post(route, {form:body}, function (error, response, body) {
console.log('error:', error);
console.log('statusCode:', response && response.statusCode);
console.log('body:', body);
return response ;
});
And I get the same logs from Foxx.
What do I do wrong ?
Here is a screenshot of my operation on the Foxx interface. Is it normal that I cannot specify a request body for testing ?
I think the reason is because you haven't specified in the end point in Foxx that there is a body expected as part of the .post.
It took me a while to work out a way of defining Foxx MicroServices, and I read through a number of ArangoDB example code before I settled on a pattern.
To help you get started, I've provided how I would quickly mock up the Foxx MicroService code in a way that is extensible, allowing you to separate your Routes from your Models.
Use these as examples to get your example working.
I've made assumptions that there are two document collections, 'Drop' and 'Bot' with an edge collection that joins them called 'VisitedBy'.
All these files are stored on your Foxx MicroService:
main.js
'use strict';
module.context.use('/v1/visitedBy', require('./routes/visitedBy'), 'visitedBy');
routes/visitedBy.js
'use strict';
const request = require('#arangodb/request');
const joi = require('joi');
const createRouter = require('#arangodb/foxx/router');
const VisitedBy = require('../models/visitedBy');
const visitedDataSchema = joi.object().required().description('Data that tracks a visited event');
const router = createRouter();
module.exports = router;
/*********************************************
* saveVisitedBy
* Path Params:
* none
* Query Params:
* none
* Body Params:
* body (required) The data that is used to record when something is visited
*/
router.post('/', function (req, res) {
const visitedData = req.body;
const savedData = VisitedBy.saveVisitedByData(VisitedBy.fromClient(visitedData));
if (savedData) {
res.status(200).send(VisitedBy.forClient(savedData));
} else {
res.status(500).send('Data not saved, internal error');
}
}, 'saveVisitedBy')
.body(visitedDataSchema, 'visited data')
.response(VisitedBy.savedDataSchema, 'The response after the data is saved')
.summary('Save visited data')
.description('Save visited data');
models/visitedBy.js
'use strict';
const _ = require('lodash');
const joi = require('joi');
const db = require('#arangodb').db;
const visitedByEdgeCollection = 'VisitedBy';
/*
Schema for a response after saving visitedBy data
*/
const savedDataScema = {
id: joi.string(),
data: joi.object(),
_from: joi.string(),
_to: joi.string()
};
module.exports = {
savedDataSchema: savedDataScema,
forClient(obj) {
// Implement outgoing transformations here
// Remove keys on the base object that do not need to go through to the client
if (obj) {
obj = _.omit(obj, ['_id', '_rev', '_oldRev', '_key']);
}
return obj;
},
fromClient(obj) {
// Implement incoming transformations here
return obj;
},
saveVisitedByData(visitedData) {
const q = db._createStatement({
"query": `
INSERT {
_from: #from,
_to: #to,
data: #data,
date: DATE_NOW()
} IN ##col
RETURN MERGE ({ id: NEW._id }, NEW)
`
});
q.bind('#col', visitedByEdgeCollection);
q.bind('from', visitedData.from);
q.bind('to', visitedData.to);
q.bind('data', visitedData.data);
const res = q.execute().toArray();
return res[0];
}
};
Your service should look like this in the Swagger interface:
You can learn more about using joi to define data structures here.
It takes a bit getting used to joi, but once you get some good working examples you can define great data definitions for incoming and outgoing data.
I hope this helps, it was difficult for me getting a basic MicroService code model that made it clear how things operated, I'm sure a lot can be done for this example but it should be a good starting spot.
As David Thomas explained in his answer, I needed to specify a body format in my router code (Foxx side).
In short :
const bodySchema = joi.object().required().description('Data Format');
router.post('/path/to/:param', function (req, res) {
var data = req.body ;
var result = api.DoSomething (req.stateParams.param, data)
res.send(result)
})
.body(bodySchema, 'Body data')
.response(joi.object().required(), 'Entry stored in the collection.')
.summary('Summary')
.description('Description')

Get model from mongoose db

I'm currently looking into building a small REST based service to which I can POST some data into a mongoose db and GET the data back.
Here's my main.js file:
var http = require("http");
var DAO = require("./DAO");
var express = require("express");
var util = require('util');
var app = express();
app.use(express.bodyParser());
app.post('/postIsles',function(req,res){
DAO[req.method](req.body);
res.send("body" + req.body.name);
});
app.get('/getIsles',function(req,res){
var isleVar = DAO[req.method](req);
res.send(isleVar);
});
app.listen("3000");
console.log("\nApp available at http://127.0.0.1:3000\n");
And DAO.js:
var mongoose = require('mongoose');
//Connect to database
mongoose.connect( 'mongodb://127.0.0.1:27017/library_database' );
//Schemas
var Isle = new mongoose.Schema({
name: String,
description: String,
lastStocked: Date
});
//Models
var IsleModel = mongoose.model( 'Isle', Isle );
function POST(request) {
var name = request.name;
var description = request.description;
var lastStocked = request.lastStocked;
console.log("POST REQ +" + request);
var isle = new IsleModel({
name: name,
description: description,
lastStocked: lastStocked
});
isle.save( function( err ) {
if( !err ) {
return console.log( 'created' );
} else {
return console.log( err );
}
});
}
function GET(request) {
return IsleModel.find( function( err, islesT ) {
if( !err ) {
console.log("isles :"+islesT);
return islesT;
} else {
return console.log( err );
}
});
}
exports.POST = POST;
exports.GET = GET;
When I try to run the GET, I get the following error:
TypeError: Converting circular structure to JSON
at Object.stringify (native)
I'm a bit unsure how to overcome this.
Remember when using Node.js: any operation that involves IO will be asynchronous.
Model#find is an asynchronous method, so isleVar is not set to the result you're expecting. Your result will only be available inside of the anonymous function that you pass into IsleModel.find
To fix your GET method, you'll need to modify your code to take into account the asynchronicity of the DB request and only send the response once your app has had a chance to retrieve data.
Below, is an example of one possible solution to fix /getIsles:
In main.js, modify your get route to pass in res (so it can be handled asynchronously)
app.get('/getIsles',function(req,res){
return DAO[req.method](req, res);
});
In DAO.js, have response send the data inside of your callback to IsleModel.find
function GET(request, response) {
IsleModel.find( function( err, islesT ) {
if( !err ) {
console.log("isles :"+islesT);
response.send(islesT);
} else {
return console.log( err );
}
});
}

Resources