Catalyst Built-in Support for HTTP Method Dispatching
In the recent release of Catalyst we added new action matching attributes for your controllers so that you can specify matches on http methods. Here's an example:
package MyApp::Web::Controller::Users;
use Moose;
use MooseX::MethodAttributes;
extends 'Catalyst::Controller';
sub start : ChainedParent
PathPrefix CaptureArgs(0)
{
my ($self, $ctx) = @_
}
sub list_users : Chained('start')
GET PathPart('') Args(0) { }
sub add_users : Chained('start')
POST PathPart('') Args(0) { }
__PACKAGE__->meta->make_immutable;
This controller would match both "GET /users" and "POST /users".
Previously similar functionality was granted via the external distributions Catalyst::Action:REST and Catalyst::ActionRole::MatchMethods. Main additions in the cored version includeHTTP method matching at any part of the chain and you can have more than one match per action:
package MyApp::Web::Controller::Users;
use Moose;
use MooseX::MethodAttributes;
extends 'Catalyst::Controller';
sub start : ChainedParent
GET POST PathPrefix CaptureArgs(0)
{
my ($self, $ctx) = @_
}
sub list_users : Chained('start')
GET PathPart(users) Args(0) { }
sub add_users : Chained('start')
POST PathPart(users) Args(0) { }
__PACKAGE__->meta->make_immutable;
Currently supported methods with shortcuts are GET, POST, PUT, DELETE, HEAD, OPTIONS and PATCH. However we can support lesser used methods via the extended syntax:
sub : Method(COPY) ChainedParent Args(0) { ... }
In addition, since many web browsers do not support a large HTTP method vocobulary, we support the following extended http headers which can be used to 'override' the actual http method:
- X-HTTP-Method (Microsoft)
- X-HTTP-Method-Override (Google/GData)
- X-METHOD-OVERRIDE (IBM)
These overrides are commonly used in some client side Javascript frameworks, and are given here for compatibility purposes. I personally think they are not the best idea, but I guess they are practical so we support it.
The current implementation does NOT do any of the following which you might expect in a more correctly RESTful system:
- Automatic setting of the HTTP Allows header based on declared HTTP Methods
- Default "Not Allowed" actions when you hit an endpoint with declared HTTP Methods with a non matching method
We defer working on support for that to a later release. For now you will need to handle that manually:
sub yes : POST Chained('start') PathPart('endpoint') Args(0)
{
my ($self, $ctx) = @_;
$ctx->res->body('AOK!');
}
sub no : Chained('start') PathPart('endpoint') Args(0)
{
pop->res->body('NOT ALLOWED')
}
Remember that when chaining, place your most specific matches at the top of the current chain level and your defaults or 'catchall' last. This is because Chaining http methods works via a 'in a first match wins' (or is does for matches that use the action 'match' and 'match_captures' methods.
Thanks to our contributors (dpetrov++) this http matching info is available in the bootup debugging screen that is built into Catalyst:
[debug] Loaded Chained actions:
.-------------------------------------+--------------------------------------.
| Path Spec | Private |
+-------------------------------------+--------------------------------------+
| /api/yes | /start (0) |
| | -> /api/start (0) |
| | => /api/no |
| /api/test | /start (0) |
| | -> /api/start (0) |
| | => GET /api/test |
| /api/yes | /start (0) |
| | -> /api/start (0) |
| | => POST /api/yes |
| / | /start (0) |
| | => /index |
'-------------------------------------+--------------------------------------'
It is not yet however reported in the 'running' debug you might be familiar with:
[debug] "POST" request for "api/yes" from "127.0.0.1"
[debug] Response Code: 200; Content-Type: application/json; Content-Length: 1
[info] Request took 0.022284s (44.875/s)
.------------------------------------------------------------+-----------.
| Action | Time |
+------------------------------------------------------------+-----------+
| /start | 0.000287s |
| /api/start | 0.000185s |
| /api/yes | 0.000095s |
| /end | 0.000265s |
'------------------------------------------------------------+-----------'
You will note that although the HTTP Method match is not reported in the Action table, it is reported as a debug line, typically 2 or 3 lines up (unless you've added a lot of your own debugging messages).
This completes a summary of these new features in the latest version of Catalyst.
Why was this added to core?
Although Catalyst strives to be unopinionated and generally prefers features in the extended ecosystem, most newer web frameworks have at least some minimal support for dispatching based on HTTP methods. I think this makes sense given the rise of RESTful architectural principals in web development and the fact that more rich client side frameworks exist, leading us to need better support for this type of server side work if we wish Catalyst to remain a good choice in the future.
In addition having this feature in core will make it much easier for us to improve it since we can introspect the entire dispatcher more easily. For example, we can do things like properly populate HTTP-Allowed, created default 'Method Not Allowed" actions and enhance the debugging screens. Given the way Catalyst Action chains work I think this would have been hard to do as a ControllerRole. Lastly there is a bit of a discovery process to the greater Catalyst ecosytem on CPAN which is not always obvious to people new to the framework.
What's Next for Catalyst?
Here's some thoughts on things that I think are short but achievable goals, that would have real value for the community. Right now I think it would be better for us to focus on these smaller and focused tasks so that we can start to build up code competency amongst a group of contributors, rather than try to take on big, long lived task that might never stablize enough to sent to CPAN.
I would like to continue adding into core some reasonable and minimal features related to RESTful architectural styles. Additionally we'd like to complete some refactors and deprecation cycles that have been long planned. Here's the brain dump:
Remove support for Regexp style dispatching from core and move it to a stand alone package on CPAN
We've said for a LOOOOOONG time that regexp style dispatching is a bad idea, and for the most part can be replaced by chaining. Lets remove Catalyst::DispatchType::Regexp from core, move it to a standalone distribution with docs saying it exists only for legacy support. It would also be a good step to change Catalyst::Controller to notice the Regexp method attributes and complain if you have not installed the legacy DispatchType. In a future version of Catalyst we will totally clean that up but we should at least take this first step.
Migrate remaining DispatchTypes to use Chaining under the hood.
Having only the Chained dispatch type is going to make it easier for us to think about deeper changes to the Dispatcher by reducing the amount of code we need to support. There's already a branch out there, so this is really about testing and deep thinking (and broad community acceptance).
Basic support for matching on HTTP Accept
Another baby step toward improving Catalyst's ability to live in a RESTful world is to add some minimal support for declarative dispatch based on the request's HTTP-Accept header. This would not yet be full on support for content negotiation, but it would allow one to declaratively dispatch based on what the request is providing. He's an early draft of what that could look like:
package MyApp::Web::Controller::Users;
use Moose;
use MooseX::MethodAttributes;
extends 'Catalyst::Controller';
sub start : ChainedParent
PathPrefix CaptureArgs(0)
{
my ($self, $ctx) = @_
}
sub list_users : Chained('start')
Accept(HTML) PathPart(users) Args(0) { }
sub add_users : Chained('start')
Accept(JSON) PathPart(users) Args(0) { }
__PACKAGE__->meta->make_immutable;
I've written an ActionRole that lives on CPAN that does something like this : https://metacpan.org/module/Catalyst::ActionRole::MatchRequestAccepts but it only works on endpoints and could use a bit more thinking.
Add a ->uri method to Controller->action_for
We all know that its ideal to create URLs using the built in URL generation features. This makes it possible to both change your URL structure without worrying about breaking links, and it also helps you to avoid creating incorrect links. However there's just way too much boilerplate:
$c->uri_for_action($c->controller('SomeController')->action_for('myaction'), \@captures, @args, \%query)
Other frameworks let you hung a URI directly off the action or controller
$c->controller('SomeController')->action_for('myaction)->uri(@captures_and_args, \%query);
Another upside is that when creating a link for an action in the same controller you don't need to have access to the context (I think...)
$self->action_for('myaction)->uri(@captures_and_args, \%query);
Although this is a small thing I think making it easier to do the best practice is never a bad idea.
In addition, I think some discussion and/or prototypes around the following as to generate support toward a solution we'd like in a future version of Catalyst:
Prototype for more grainular control of HTTP POST/PUT body parsing
Currently Catalyst only supports classic HTML style form posting and multipart uploads. It is increasingly common to support at least JSON since most client side Javascript frameworks promote that. In addition the build in form post parsing has some know issues differentiating parameters that have the same name, and also is does not support any of the most commonly used idioms for deeply nested parameters.
Catalyst::Action::REST has a controller that does basic bulk deparsing of incoming parameters, but it is a heavy weight solution which is not core to Catalyst. There are also a handful of Request traits that do some of this but on a global level.
I'm proposing that we adopt a system similar to Scala's Playframework, which lets you declare a body paser per action. I think this would give is more granular control, and let you have a lot more power (Scala Play has an example of a parser that stores the incoming in a Amazon S3 bucket for example).
For now we'd leave the HTTP Body parsing code in Catalyst Request alone, which is safe I think since it only gets called if you request it (in other words no change of double parsing a body)
package MyApp::Web::Controller::Users;
use Moose;
use MooseX::MethodAttributes;
extends 'Catalyst::Controller';
sub start : ChainedParent
PathPrefix CaptureArgs(0)
{
my ($self, $ctx) = @_
}
sub add_user : Chained('start')
BodyParser(JSON) PathPart(users) Args(0)
{
my ($self, $ctx, %parsed_body) = @_;
}
__PACKAGE__->meta->make_immutable;
In this example I also suggest that it might be nice if we'd tack the parsed body onto the end of the action arguments. Given that getting body parameters is one of the more common things I see in controllers (even if I think there's a better approach, it si the common thing do it) perhaps it would make sense to let us optimize a bit for the common case, and not force us to reach for $ctx->request->body_parameters->... just to get them?
Another possibility here is to follow Web::Simple's example and localize %_ to the parsed parameters. That might be safer than messing around with @args.
In any case we could have a default body parser similar to Scala Play that does all the most common things, html forms, json, xml, and so forth. The main downside is that I am not sure we'd want to force Catalyst to need a JSON parser in the dependencies (Catalyst already gets a lot of flack for requiring a lot of dependencies). Your thoughts?
Prototype for better use of plack middleware
Although Catalyst is now Plack based, I really don't think we are taking full advantage of the Plack/ PSGI ecosystem. One thing that come to mind would be to improve our ability to use plack applications and middleware, such at in the future we could depracate some of our older Catalyst plugins and just use the Plack stuff. Another step would be to improve Catalyst's support for Placks nonblocking and deferred responses, but I think that might have to wait a bit.
There's a few things on CPAN around the idea of being able to use Plack more fully in Catalyst. I've written two plugins that munge support for adding middleware and applications directly into the core Catalyst application:
https://metacpan.org/release/Catalyst-Plugin-EnableMiddleware
https://metacpan.org/release/Catalyst-Plugin-URLMap
Also, frew has written an action class that exposes Plack superpowers directly in your action:
https://metacpan.org/release/Catalyst-Action-FromPSGI
I think supporting something like these in core would be a great step toward making Catalyst play nicer with the greater Plack ecosystem. I also don't think it would be hard to do, we just need to ponder the interfaces a bit to make sure we have something worth supporting over the longer term. Another great upside is that it could lead us toward using more Plack middleware in core, and let us not have to support some of the Catalyst plugins that overlap with Plack middleware, such as authentication, sessioning, and so forth. Although those plugins are widely used right now, I think it would be ideal if we could leverage the full community instead of maintaining Catalyst only versions of this common infrastructure.
More declarative HTTP Status messages might be fruitful.
This is the least fully formed thought, but around the idea of improving declaritive support for general RESTful design, I was thinking some straightforward way of declaring the response Status of an action would be useful.
package MyApp::Web::Controller::Users;
use Moose;
use MooseX::MethodAttributes;
extends 'Catalyst::Controller';
sub start : ChainedParent
PathPrefix CaptureArgs(0)
{
my ($self, $ctx) = @_
}
sub list_users : Chained('start')
Ok PathPart(users) Args(0) { }
__PACKAGE__->meta->make_immutable;
Or possibly:
package MyApp::Web::Controller::Users;
use Moose;
use MooseX::MethodAttributes;
use Catalyst::Controller::StatusCodes;
extends 'Catalyst::Controller';
sub start : ChainedParent
PathPrefix CaptureArgs(0)
{
my ($self, $ctx) = @_
}
sub list_users : Chained('start')
PathPart(users) Args(0)
{
Ok 'templates/list-users.tt'
}
__PACKAGE__->meta->make_immutable;
We'd need a few rational use cases and test cases to see if this would have real value. My thinking here is that I have a long term goal to reduce the need for controllers and actions to reach deep down into the context to do common things like perform redirects and so forth.
In the Greater CPAN ecosystem
I'd like to see the venerable TT view fully converted to Moose, add an attribute to let you control the default content-type (right now by default it uses text/html, which you need to override in you action if you are using a different format. Also, I think it would be neat if we could 'sugar up' the expose_methods support so that you don't need to manually update configuration:
package MyApp::Web::View::HTML;
use Moose;
use MooseX::MethodAttributes;
extends 'Catalyst::View::TT';
sub uri_for : Helper {
my ($self, $ctx, @args) = @_;
$ctx->uri_for(@args);
}
As a bonus++, adding something similar to 'sugar up' adding Filter and Virtual methods might re-energise the vernerable TT view a bit. Oh, and I always thought it might be cool if you could have view helpers on a controller level which might prevent having lots of irrelavent helpers in the view...
package MyApp::Web::Controller::Users;
use Moose;
use MooseX::MethodAttributes;
extends 'Catalyst::Controller';
sub start : ChainedParent
PathPrefix CaptureArgs(1)
{
my ($self, $ctx) = @_;
}
sub list_users : Chained('start')
PathPart(users) Args(0) { }
sub some_helper_method : ViewHelper
{
my ($self, $ctx, @args) = @_;
}
__PACKAGE__->meta->make_immutable;
I actually think it would not be so hard to do, although it might be tricky to make it perform well. Anyway, it would be as they say, 'a reach goal' :)
Looking forward to your comments, suggestions and improvement. Oh, and volunteers because there's no way I am going to do all this on my own :)
Comments
You can follow this conversation by subscribing to the comment feed for this post.