June 2015 Archives

Dancer2 Named Route Parameters

If you're familiar with Dancer2 (my favorite Perl web framework to date), you know how amazingly easy it is to develop self-describing routes for your web application.

Let's say you have a Dancer2 app that allows your users to manage their phone numbers in a 1:X relationship. You might want to expose a route like...

post '/user/:user_id/phone_numbers/new' => sub {
    my $user_id = param 'user_id';
    # Do some stuff
};

... to add a new number to their profile. Let's also assume you have a route upstream that prohibits users from accessing routes associated with other users:

any '/user/*/**' => sub {
    my ($user_id) = splat;
    send_error(403) unless $user_id == session('user_id');
    pass;
};

Looks good, right?

If you have read all the documentation associated with Dancer2 (of course you did), you might have noticed params can take an optional argument to limit the source of the parameters. In other words, you can get parameters defined by the route, the query string (GET), or the content body (POST) in isolation.

What source does param use then? Well, it defaults to the behavior of params without arguments, which is to say that parameters from all three sources are munged together into a hash using the parameter name as a key.

What this means is that parameters from one source may be overwritten (not collected together in an array) by another source. Specifically, POST arguments overwrite all parameters of the same name, and named route parameters overwrite GET query string parameters of the same name.

So, back to our example!

any '/user/*/**' => sub {
    my ($user_id) = splat;
    send_error(403) unless $user_id == session('user_id');
    pass;
};

post '/user/:user_id/phone_numbers/new' => sub {
    my $user_id = param 'user_id';
    # Do some stuff
};

In this well-intended code, what is not accounted for is whether a POST argument might be received of the name user_id. Sure, we might not write such a conflict into our application but a less well-intentioned visitor might.

Documented behavior or not, this behavior is not how my brain works. So to accommodate my own laziness, I posted Dancer2::Plugin::ParamKeywords to facilitate more explicit parameter gathering and munging. So now the above example can be rewritten as...

any '/user/*/**' => sub {
    my ($user_id) = splat;
    send_error(403) unless $user_id == session('user_id');
    pass;
};

post '/user/:user_id/phone_numbers/new' => sub {
    # New keyword, route_param
    my $user_id = route_param 'user_id';
    # Do some stuff
};

... which will do what you would expect it to.

Stay tuned for a new parameters keyword that will eliminate the ambiguity described in this blog post in a future Dancer2 release. Also, thanks to XSAWYERX and MICKEY for an excellent YAPC::NA master class on Dancer2!

About Camspi

user-pic Yet another Perl enthusiast! <3