Loopback: How to format the response returned to client ?

Created on 8 Oct 2014  路  18Comments  路  Source: strongloop/loopback

Hi,

I would like to re-format the response returned to client.

I know we could use afterRemote hook, but if doing so, I have to do it every single Models, one by one.

Is there any other solution in that I only need to change the code at place and it will be applied to all Models, something likes that ?

Thanks for any help.

Most helpful comment

Is there any other solution in that I only need to change the code at place and it will be applied to all Models, something likes that ?

You can use a strong-remoting after hook...

// in my-project/server/boot/hook.js
module.exports = function(server) {
  var remotes = server.remotes();
  // modify all returned values
  remotes.after('**', function (ctx, next) {
    ctx.result = {
      data: ctx.result
    };

    next();
  });
};

All 18 comments

Is there any other solution in that I only need to change the code at place and it will be applied to all Models, something likes that ?

You can use a strong-remoting after hook...

// in my-project/server/boot/hook.js
module.exports = function(server) {
  var remotes = server.remotes();
  // modify all returned values
  remotes.after('**', function (ctx, next) {
    ctx.result = {
      data: ctx.result
    };

    next();
  });
};

@crandmck can you add this to the docs?

@ritch: thanks

See #634

@ritch actually, the hook is not triggered in some case "error" response returned, just note that, it works properly in case of successful response.

for example about error response of POST /Users/login end-point

{
  "error": {
    "name": "Error",
    "status": 401,
    "message": "login failed",
    "statusCode": 401,
    "stack": "Error: login failed\n    at ...."
  }
}

I added the hook.js into /server/boot folder.

module.exports = function(server) {
  var remotes = server.remotes();
  console.log('enter #1');

  remotes.before('**', function (ctx, next, method) {
    console.log('enter #2');
    next();
  });

  // modify all returned values
  remotes.after('**', function (ctx, next) {
    console.log('enter #3', ctx);
    next();
  });
};

I run server with "slc run"

in the case of built-in User, after requesting POST /Users/login with some invalid data to get error response

{
"username":"mabu",
"password":"songoku"
}

I check the console and found only

....
enter #1
Browse your REST API at http://localhost:3000/explorer
Web server listening at: http://localhost:3000/
enter #2

no log for "enter #3", it means the hook is not triggered as expected, is it a bug ?

Note:

  • it happens with afterRemote('**',...) too.
  • I am using loopback v2.4.1

not sure is this the reason why the after hook doesn't work in error response

/loopback/node_modules/strong-remoting/lib/rest-adapter.js

RestAdapter.errorHandler = function(options) {
  options = options || {};
  return function restErrorHandler(err, req, res, next) {
    if(typeof err === 'string') {
      err = new Error(err);
      err.status = err.statusCode = 500;
    }

    res.statusCode = err.statusCode || err.status || 500;

    debug('Error in %s %s: %s', req.method, req.url, err.stack);
    var data = {
      name: err.name,
      status: res.statusCode,
      message: err.message || 'An unknown error occurred'
    };

    for (var prop in err) {
      data[prop] = err[prop];
    }

    data.stack = err.stack;
    if (process.env.NODE_ENV === 'production' || options.disableStackTrace) {
      delete data.stack;
    }
    res.send({ error: data }); // IT SENDS ERROR DATA TO CLIENT DIRECTLY ?
  };
};

@bajtos do you have any idea about this issue ?

@projectxmaker Your conclusion seems correct to me.

There is a pending pull request that will allow you to provide a custom error response handler - see https://github.com/strongloop/strong-remoting/pull/114. Will that solve your problem?

@bajtos thanks for your support, but above fix doesn't resolve my issue.

I modified rest-adapter.js as you suggested

RestAdapter.errorHandler = function(options) {
  options = options || {};
  return function restErrorHandler(err, req, res, next) {
    if (options.handler) {
      return options.handler(err, req, res, next)
    }

    if(typeof err === 'string') {
      err = new Error(err);
      err.status = err.statusCode = 500;
    }

    res.statusCode = err.statusCode || err.status || 500;

    debug('Error in %s %s: %s', req.method, req.url, err.stack);
    var data = {
      name: err.name,
      status: res.statusCode,
      message: err.message || 'An unknown error occurred'
    };

    for (var prop in err) {
      data[prop] = err[prop];
    }

    data.stack = err.stack;
    if (process.env.NODE_ENV === 'production' || options.disableStackTrace) {
      delete data.stack;
    }
    res.send({ error: data });
  };
};

but the after hook is still not triggered :(

hence, maybe it's just because I forgot to set "handler", more exactly, I don't know how can I set handler so that it could be used in loopback/lib/application.js

app.remotes = function () {
  if(this._remotes) {
    return this._remotes;
  } else {
    var options = {};

    if(this.get) {
      options = this.get('remoting'); // HOW ?
    }

    return (this._remotes = RemoteObjects.create(options));
  }
}

is it the reason why your solution cannot solve my issue so far ?

 options = this.get('remoting'); // HOW ?

Option A:

// in you server/server.js
var opts = app.get('remoting') || {};
opts.handler = function(...){};
app.set('remoting', opts);

Option B (may not work as I expect, requires https://github.com/strongloop/loopback-boot/pull/54 - [email protected]):

// in server/config.local.js
module.exports = {
  remoting: {
    handler: function(){ ... }
  }
};

but the after hook is still not triggered

Of course it won't be triggered, this is a different mechanism.

I suppose we can add remotes.afterError hook allowing users to customise error objects, although I am not sure if that's a desirable feature. Feel free to open a new GH issue to discuss that idea.

@bajtos thanks for your support :)

it seems I have to stop thinking about Re-formating Response of Loopback API.

just thought it's just simple to do after hook as @ritch suggested, but it is to be too difficult so far O.o

@bajtos just share something I found

in strong-remoting/lib/rest-adapter.js, the input data for RestAdapter.errorHandler is "this.remotes.options.errorHandler

 RestAdapter.prototype.createHandler = function () {
  ....
  // Use our own error handler to make sure the error response has
  // always the format expected by remoting clients.
  root.use(RestAdapter.errorHandler(this.remotes.options.errorHandler));

  return root;
};

then, if I set up handler as your suggestion in /server/server.js

// in you server/server.js
var opts = app.get('remoting') || {};
opts.handler = function(...){};
app.set('remoting', opts);

following code line in function RestAdapter.errorHandler of /strong-remoting/lib/rest-adapter.js never works as our expect, specifically options.handler is never returned because "options.errorHandler" doesn't have any "handler" property.

RestAdapter.errorHandler = function(options) {
  options = options || {};

  if (options.handler) return options.handler;

  ....
};

SO, I re-defined what you suggested to change in /server/server/js as following

// in you server/server.js
var opts = app.get('remoting') || {};
opts.errorHandler = {
  'handler': function restErrorHandler(err, req, res, next) {
              ....
            }
}
app.set('remoting', opts);

another solution is to put this code into a hook in /server/boot/hook.js

module.exports = function(server) {
  var remotes = server.remotes();
  var opts = remotes.options || {};
  opts.errorHandler = {
    'handler': function restErrorHandler(err, req, res, next) {
                ....
              }
  }
  remotes.options =  opts;
}

thanks for your support :+1:

How to format the response returned from Remote methods to client?

How to response just a String from Remote methods to client?

module.exports = function(Test) {

    Test.teamMembers = function(sthId, aName, cb) {
        cb(err, 'want to return just a String')
    }


    Test.remoteMethod('format', {
        accepts: [
            {arg: 'sthId', type: 'string'},
            {arg: 'aName', type: 'string'}
        ],

        http: {verb: 'get'},
        returns: {type: 'string'}

    })

}

Hey,
Maybe im too late but from the remote method documentation on loopback i could find this:

Formatting remote method responses
You can reformat the response returned by all remote methods by adding a boot script that modifies the object returned by app.remotes() as follows:

// /server/boot/hook.js
module.exports = function(app) {
  var remotes = app.remotes();
  // modify all returned values
  remotes.after('**', function (ctx, next) {
    ctx.result = {
      data: ctx.result
    };

next();

  });
};

If you just want to format all errors then make handler in server.js.

app.get('remoting').errorHandler = {
  handler: function(error, req, res, next) {
    console.log('follow me');
    var log = require('debug')('server:rest:errorHandler');
    if (error instanceof Error) {
      log('Error in %s %s: errorName=%s errorMessage=%s \n errorStack=%s',
        req.method, req.url, error.name, error.message, error.stack);
    }
    else {
      log(req.method, req.originalUrl, res.statusCode, error);
    }
    next(); /\* let the default error handler (RestAdapter.errorHandler) run next */
  },
  disableStackTrace: true
};

Thanks @virendra656
In my case, I needed to intercept well meant MongoError instances that shouldn't be served with status 500, they were more 409 (ish). This worked perfecly.

As note, for Loopback 3.X

// /server/boot/wrap-error.js
module.exports = function (app) {
  var remotes = app.remotes();

  remotes.options.rest = remotes.options.rest || {}
  remotes.options.rest.handleErrors = false;

  app.middleware('final', FinalErrorHandler);

  function FinalErrorHandler(err, req, res, next) {
    res.status(400).send({
      code: 400,
      message: err.message,
      data: {},
      result: "fail"
    }).end();
  }
};
// /server/boot/wrap-success.js
module.exports = function (app) {
  var remotes = app.remotes();

  remotes.after('**', function (ctx, next) {
    ctx.result = {
      code: 200,
      message: ctx.methodString + " success",
      data: ctx.result,
      result: "success"
    };

    next();
  });
};
Was this page helpful?
0 / 5 - 0 ratings