8

Imagine a web application with routes that need to check whether the user is allowed to access a given resource before proceeding. The "is authenticated" check relies on a database call.

In each route, I may have:

authorizeOwnership(req, res)
.then(function() {
    // do stuff
    res.send(200, "Yay");
});

I want the authorizeOwnership() function to handle 403 (access denied) and 500 (e.g. database query error) responses so that each route doesn't need to do so explicitly.

I have a function that can query the database to check for ownership.

function confirmOwnership(resourceId, userId) {
    // SequelizeJS... returns a bluebird promise
    return Resource.find({
        where: {id: resourceId, userId: userId}
    })
    .then(function(resource) {
        if(!resource) {
            return null; // no match for this resource id + user id
        } else {
            return resource;
        }
    });
}

This is then used in authorizeOwnership:

function authorizeOwnership(req, res) {
    var rid      = parseInt(req.params.rid, 10),
        userId   = parseInt(req.authInfo.userid, 10);

    return new Promise(function(resolve, reject) {
        confirmOwnership(rid, userId)
        .then(function(resource) {
            if(resource === null) {
                res.send(403, "Forbidden");
                // Note: we don't resolve; outer handler will not be called
            } else {
                resolve(resource);
            }
        })
        .catch(function(err) {
            console.log(err);
            res.send(500, "Server error");
            // Note: we don't resolve; outer handler will not be called
        });
    });
}

In this scenario, I deliberately don't call reject() or resolve() in some of the code paths. If I do, then my "outer" route logic (the code that's calling authorizeOwnership()) gets more complicated because it has to handle errors (either with a .catch() or with a null check in .then()).

Two things make me a bit nervous though:

  • Is it OK for the promise returned by authorizeOwnership() to never be resolved in error scenarios? Will it cause a delay or a memory leak?

  • Is it logically sound to resolve confirmOwnership() with null to say "no matching resource found" and then treat that as an error in authorizeOwnership()? My first attempt rejected the promise in confirmOwnership() when there was no matching resource, but this made things more complicated because it is difficult to distinguish between this (the 403 case) and an actual error (the 500 case).

optilude
  • 3,538
  • 3
  • 22
  • 25
  • 1
    On a general note: please do not make 'god' routines like this: 'authorizeOwnership() function to handle 403 (access denied) and 500. They should need permission from the caller to be allowed to handle errors or not (a parameter to return errors or not). The manager (caller) always knows more about the context than the 'called' routine'. – Ryan Vincent Apr 12 '14 at 23:18
  • Adding my "Yes" to what @RyanVincent said. The routine that queries the data base should not make application level decisions. Every component in your program should do exactly one thing. These things can get really out of control if you don't structure who accesses the state of a request very carefully. – Benjamin Gruenbaum Apr 12 '14 at 23:25
  • I agree with what you're saying, though the example above is a simplification of the actual code for the purpose of making the question clear, and the actual code doesn't suffer from the problems you're suggesting. – optilude Apr 13 '14 at 16:33

1 Answers1

5

Is it OK for the promise returned by authorizeOwnership() to never be resolved in error scenarios? Will it cause a delay or a memory leak?

Yes, it is safe to not resolve a Bluebird promise (and to be fair, any other implementation I've checked -pretty pictures here). It has no global state on its own.

The question of whether or not it's good practice is different. It's like a synchronous break in a sense. Personally I'm not a fan.

Is it logically sound to resolve confirmOwnership() with null to say "no matching resource found" and then treat that as an error in authorizeOwnership()?

This depends on your API. It's again an opinion. It works, but I probably would not return null but indicate failure if the case is exceptional. You can distinguish the rejection using an error object. You can reject with an AuthorizationError object you create for example. Note Bluebird also supports typed catches.

Something like:

// probably shouldn't send the response to authorizeOwnership but use it externally
// to be fair, should probably not take req either, but rid and userid
var authorizeOwnership = Promise.method(function(req) {
    var rid      = Number(req.params.rid),
        userId   = Number(req.authInfo.userid;
        return confirmOwnership(rid, userId); // return the promise
    });
});

function ServerError(code,reason){
    this.name = "ServerError";
    this.message = reason;
    this.code = code;
    Error.captureStackTrace(this); // capture stack
}
var confirmOwnership = Promise.method(function(resourceId, userId) {
    // SequelizeJS... returns a bluebird promise
    return Resource.find({
        where: {id: resourceId, userId: userId}
    })
    .then(function(resource) {
        if(!resource) {
            throw new ServerError(403,"User not owner"); // promises are throw safe
        }
        return resource;
    });
});

Next, in your server, you can do something like:

app.post("/foo",function(req,res){
     authorizeOwnership(req).then(function(){
          res.send(200, "Owner Yay!");
     }).catch(ServerError,function(e){
            if(e.code === 403) return res.send(403,e.message);
            return res.send(500,"Internal Server Error");
     });
});

Note: you're also using the deferred anti pattern in your code. There is no need to do new Promise(function(...){ in your code, you can simply return the promise.

Community
  • 1
  • 1
Benjamin Gruenbaum
  • 270,886
  • 87
  • 504
  • 504
  • Benjamin, this looks like seriously good stuff but there's one thing I don't quite follow. If the line `throw new ServerError(403,"User not owner");` is the only place that `ServerError` is called, then surely would-be 500 errors won't penetrate the catch as written? I'm probably missing something but the [Bluebird documentation](https://github.com/petkaantonov/bluebird/blob/master/API.md#catchfunction-errorclassfunction-predicate-function-handler---promise) suggests that a "generic" catch should be chained, viz `.catch(ServerError, handler1).catch(handler2)`. – Roamer-1888 Apr 13 '14 at 02:22
  • @Roamer-1888 yes are correct, the implicit assumption not shown here is that code that shoud cause an internal server error should throw a `ServerError(500,...` generally, if I have a programmer syntax error or reference error - I don't want to return a 500 response, I want to know immediately. Bluebird will pick up on that and log it. – Benjamin Gruenbaum Apr 13 '14 at 03:38
  • Hmm, to clarify my last comment - I'd still return a 500 in real code - but not from the route handler but through global middleware. Generic catch-alls are really risky and introduce a lot of problems like catching programmer errors I didn't think about :) – Benjamin Gruenbaum Apr 13 '14 at 04:03
  • Thanks for the explanation Benjamin. On the surface it doesn't seem inappropriate for something like `.catch(ServerError, function(e) { res.send(e.code, e.message); }).catch(function(e) { res.send(500, e.messsage || "Internal Server Error"); });`. My concern is the need for overt catching of any non-ServerError errors at this point in the code. But maybe I would change my mind if I understood 'global middlewear'. – Roamer-1888 Apr 13 '14 at 08:22
  • @Roamer-1888 let's say you have a `myArray.forEch(function(...)` (note the typo) in your code, and that code is in an EJS template or some path that gets executed rarely or that sort of thing. You have a programmer bug that will now silently get ignored (since you catch `TypeError`s) - if you don't add that catch. Bluebird will detect it is uncaught and will give you a nice log. You can detect unhandled rejections with the `Promise.onPossiblyUnhandledRejection` handler or log it. – Benjamin Gruenbaum Apr 13 '14 at 08:40
  • 1
    @Roamer-1888 We have a better solution in the works via `Promise.using` giving the promise chain 'ownership' of the response which would allow for it to send the 500 via a disposer _and_ send an unhandled rejection which would show the error explicitly. However, at the moment for the very least you need to do: `catch(function(e) { res.send(500, e.messsage || "Internal Server Error"); throw e; });` so the error keeps propagating and gets logged (like exceptions in synchronous code) or explicitly log it (not as good imo). – Benjamin Gruenbaum Apr 13 '14 at 08:42
  • Benjamin, thanks for that. I wonder if there's a place for `.butterFingersCatch()` ie a non-masking catch that automatically re-throws the error. ("Butter fingers" is a term often heard on the cricket pitch when the ball is only half caught). – Roamer-1888 Apr 13 '14 at 09:08
  • @Roamer-1888 you can suggest that in the Bluebird github tracker. We can discuss this in https://www.irccloud.com/#!/ircs://irc.freenode.net:6697/%23promises . Stackoverflow tends to not like discussion in comments very much :) – Benjamin Gruenbaum Apr 13 '14 at 10:14
  • Benjamin, yes I know, but this is a great breeding ground for ideas. Thanks for the link. – Roamer-1888 Apr 13 '14 at 13:52
  • @Roamer-1888 well, to be fair - there's me here, and then there's (plain ol') me, Petka (Bluebird author), Domenic (spec co-author and promises evangalist), Kris (Q author), Stef (RSVP author), spion (lots of promise libraries and Bluebird contributer), and a bunch of other ballers at #promises :) – Benjamin Gruenbaum Apr 13 '14 at 13:55
  • Thank you, @BenjaminGruenbaum! A couple of comments/questions though: (a) the reason I have authorizeOwnership() in the first place is as a utility/template to make sure handling of unauthorised requests (for this particular reason) is done consistently across a dozen or so routes. For that reason, I don't want to have the the res.send() bit in the top level route as you've shown. (b) When I tried to do something similar, I get "unhandled error" type exceptions if the top level doesn't have a catch. That's how I ended up with the non-resolved thing. – optilude Apr 13 '14 at 16:35
  • (It may be better to do the check in middleware and dispense with the authorizeOwnership() helper entirely, but it's a harder to do that without requiring two database queries when I also need to use the "resource" object later.) – optilude Apr 13 '14 at 16:43
  • @optilude you can use the `onPossiblyUnhandledRejection` for that, but if I were you I'd write a (slim) wrapper on top of `app.get/post/whatever` that does the expected behavior - accepts a (returned) promise, waits for it, checks the error and responds accordingly. Spion wrote such a router sample once for fun, you can get inspired by his implementation. Bluebird 2 will feature facilities to make this even simpler. – Benjamin Gruenbaum Apr 13 '14 at 17:52