1

I'm working on a homemade RBAC system with Nodejs Express, based on two levels:

  • First, verify if the user has the right role to perform this action.
  • Second, verify if the user has the right plan to perform this action.

, I create a middleware like this:

exports.can = function (resource, action) {
    return function (request, response, next) {
        action = action || request.method;
        if (!request.user) {
            return next(new errors.UnauthorizedError());
        }
        request.user.can(request.user.role, request.user.currentPack, resource, action, function (can, error) {
            if (error) return next(error);
            if (!can) {
                return next(new errors.UnauthorizedError());
            }
            return can;
        });
        return next();
    };
};

I added to my User model this method:

const rbac = new RBAC(rbacJson);
const pack = new PACK(packJson);
schema.method('can', function (role, userPack, resource, action, next) {
    let can = false;
    action = action.toUpperCase();
    can = rbac.can(role, resource, action);
    if (can) {
        can = pack.can(userPack, resource, action, function (can) {
            return next(can);
        });
    }
    return next(can);
});

In my method pack.can(...) I need to execute a mongoose query like this:

PACK.prototype.can = function (pack, resource, action, next) {
    let can = true;
    // some sequantial code
    Trader.count({/* some conditions */}, function (err, count) {
         if(count == 0) return next(true);
         return next(false);
    });
    return can;
};

My problem is when the return of Mongoose query is next(false), I have this error:

 Error: Can't set headers after they are sent.
 at ServerResponse.OutgoingMessage.setHeader (_http_outgoing.js:356:11)
 at ServerResponse.header (/home/invoice/node_modules/express/lib/response.js:730:10)
 at ServerResponse.send (/home/invoice/node_modules/express/lib/response.js:170:12)
 at ServerResponse.json (/home/invoice/node_modules/express/lib/response.js:256:15)
 at ServerResponse.response.apiResponse (/home/invoice/server/config/middlewares/api.js:10:14)
 at /home/invoice/server/controllers/api/invoice/traders.js:130:21
 at /home/invoice/node_modules/mongoose/lib/model.js:3835:16
 at /home/invoice/node_modules/mongoose/lib/services/model/applyHooks.js:162:20
 at _combinedTickCallback (internal/process/next_tick.js:73:7)
 at process._tickDomainCallback (internal/process/next_tick.js:128:9)

After an investigation, I found that the error is maybe due to the double callback Call:

  • can = pack.can(userPack, resource, action, function (can) { return next(can); });
  • return next(new errors.UnauthorizedError());

But I don't know how to solve this. I hope that I well explained my problem.

1 Answer 1

2

So let's start with the error:

Can't set headers after they are sent.

Nine times out of ten this is caused by trying to send two responses to the same request. The reference to headers is a little misleading, though technically true. The first thing the second response will try to do is set some headers, which will fail because the first response has already sent them back to the client.

The stack trace gives you a clear indication of where the second response originates, including filenames and line numbers. More difficult is to track down the first response, generally I'd just throw in some extra console logging to figure it out.

In the question you mention that you believe you've found the source of the problem but it looks to me like it might just be the tip of the iceberg. You've repeatedly used the same pattern and even if you fix it in one place that may not be sufficient.

Before I get into that, let's start with this:

return next();

For the purposes of this example it doesn't really matter whether you pass an error, e.g. return next(err);, the point is the same. First it invokes next(), which returns undefined. It then returns undefined from the surrounding function. In other words, it's just a convenient shorthand for this:

next();
return;

The reason why we return is to ensure that nothing else happens after we've called next(), we always try to ensure that calling next() is the last thing we do in our handler, not least because otherwise we can have bugs where we try to send the response twice.

The (anti-)pattern you've used looks a bit like this:

obj.doSomething(function() {
    return next(); // next 1
});

return next(); // next 2

Again, it doesn't really matter whether you're calling next() or next(err), it all works out much the same. The key thing to note is that the return for next 1 is just returning from the function passed to doSomething. It isn't doing anything to prevent next 2 from being hit. Both next 1 and next 2 will be called.

In places your code seems unclear about whether it's trying to be synchronous or asynchronous, using callbacks and return values at the same time. It makes it a little bit difficult to say for sure what the 'correct' code should look like. Specifically, is the value of can supposed to be returned synchronously or passed to the callback asynchronously? I suspect it's the latter but the current code seems torn between the two. It's important to ensure that you don't call next() until you're ready for the next thing to happen, so if you're waiting on a DB query you mustn't call next until that comes back.

Personally I'd rename your callbacks so they aren't all called next, I find that really confusing. When I see next I'm expecting it to be an Express next function, not just an arbitrary callback.

It's a bit of a guess but I'd suggest your middleware should look something like this:

exports.can = function (resource, action) {
    return function (request, response, next) {
        action = action || request.method;

        if (!request.user) {
            return next(new errors.UnauthorizedError());
        }

        request.user.can(request.user.role, request.user.currentPack, resource, action, function (can, error) {
            if (error) {
                next(error);
            }
            else if (can) {
                next();
            }
            else {
                next(new errors.UnauthorizedError());
            }
        });

        // Do not call next() here
    };
};

The relevant section of the User model would then be:

if (can) {
    pack.can(userPack, resource, action, function (can) {
        next(can);
    });
}
else {
    next(can);
}
Sign up to request clarification or add additional context in comments.

1 Comment

Thank you so much for your explanation. The snippet you described obj.doSomething(function() { return next(); // next 1 }); return next(); was verry helpful. "In places, your code seems unclear about whether it's trying to be synchronous or asynchronous" To respond to your question me too I'm bit confused because I do not yet understand really the concept of asynchronous.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.