The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.

NAME

Catalyst::ActionRole::Methods - Dispatch by HTTP Methods

SYNOPSIS

 sub foo : Local Does('Methods') {
   my ($self, $c, $arg) = @_;
   # called first, regardless of HTTP request method
 }

 sub foo_GET : Action {
   my ($self, $c, $arg) = @_;
   # called next, but only for GET requests
   # this is passed the same @_ as its generic action
 }

 sub foo_POST { # does not need to be an action
   my ($self, $c, $arg) = @_;
   # likewise for POST requests
 }

 sub foo_not_implemented { # fallback
   my ($self, $c, $arg) = @_;
   # only needed if you want to override the default 405 response
 }

DESCRIPTION

This is a Catalyst extension which adds additional dispatch based on the HTTP method, in the same way Catalyst::Action::REST does:

An action which does this role will be matched and run as usual. But after it returns, a sub-action will also run, which will be identified by taking the name of the main action and appending an underscore and the HTTP request method name. This sub-action is passed the same captures and args as the main action.

You can also write the sub-action as a plain method without declaring it as an action. Probably the only advantage of declaring it as an action is that other action roles can then be applied to it.

There are several fallbacks if a sub-action for the current request method does not exist:

  1. HEAD requests will try to use the sub-action for GET.

  2. OPTIONS requests will set up a 204 (No Content) response.

  3. The not_implemented sub-action is tried as a last resort.

  4. Finally, a 405 (Method Not Found) response is set up.

Both fallback responses include an Allow header which will be populated from the available sub-actions.

Note that this action role only adds dispatch. It does not affect matching! The main action will always run if it otherwise matches the request, even if no suitable sub-action exists and a 405 is generated. Nor does it affect chaining. All subsequent actions in a chain will still run, along with their sub-actions.

INTERACTION WITH CHAINED DISPATCH

The fact that this is an action role which is attached to individual actions has some odd and unintuitive consequences when combining it with Chained dispatch, particularly when it is used in multiple actions in the same chain. This example will not work well at all:

 sub foo : Chained(/) CaptureArgs(1) Does('Methods') { ... }
 sub foo_GET { ... }

 sub bar : Chained(foo) Args(0) { ... }
 sub bar_POST { ... }

Because each action does its own isolated Methods sub-dispatch, a GET request to this chain will run foo, then foo_GET, then bar, then set up a 405 response due to the absence of bar_GET. And because bar only has a sub-action for POST, that is all the Allow header will contain.

Worse (maybe), a POST will run foo, then set up a 405 response with an Allow list of just GET, but then still run bar and bar_POST.

This means it is never useful for an action which is further along a chain to have more sub-actions than any earlier action.

Having fewer sub-actions can be useful: if the earlier part of the chain is shared with other chains then each chain can handle a different set of request methods:

 sub foo : Chained(/) CaptureArgs(1) Does('Methods') { ... }
 sub foo_GET { ... }
 sub foo_POST { ... }

 sub bar : Chained(foo) Args(0) { ... }
 sub bar_GET { ... }

 sub quux : Chained(foo) Args(0) { ... }
 sub quux_POST { ... }

In this example, the /foo/bar chain will handle only GET while the /foo/quux chain will handle only POST. If you later wanted to make /foo/quux also handle GET then you would only need to add quux_GET because there is already a foo_GET. But to make /foo/bar handle PUT, you would need to add both foo_PUT and bar_PUT.

VERSUS Catalyst::Action::REST

Catalyst::Action::REST works fine doesn't it? Why offer a new approach? There's a few reasons:

First, when Catalyst::Action::REST was written we did not have Moose and the only way to augment functionality was via inheritance. Now that Moose is common we instead say that it is typically better to use a Moose::Role to augment a class function rather to use a subclass. The role approach is a smaller hammer and it plays nicer when you need to combine several roles to augment a class (as compared to multiple inheritance approaches.). This is why we brought support for action roles into core Catalyst::Controller several years ago. Letting you have this functionality via a role should lead to more flexible systems that play nice with other roles. One nice side effect of this 'play nice with others' is that we were able to hook into the 'list_extra_info' method of the core action class so that you can now see in your developer mode debug output the matched http methods, for example:

    .-------------------------------------+----------------------------------------.
    | Path Spec                           | Private                                |
    +-------------------------------------+----------------------------------------+
    | /myaction/*/next_action_in_chain    | GET, HEAD, POST /myaction (1)          |
    |                                     | => /next_action_in_chain (0)           |
    '-------------------------------------+----------------------------------------'

This is not to say its never correct to use an action class, but now you have the choice.

Second, Catalyst::Action::REST has the behavior as noted of altering the core Catalyst::Request class. This might not be desired and has always struck the author as a bit too much side effect / action at a distance.

Last, Catalyst::Action::REST is actually a larger distribution with a bunch of other features and dependencies that you might not want. The intention is to offer those bits of functionality as standalone, modern components and allow one to assemble the parts needed, as needed.

This action role is for the most part a 1-1 port of the action class, with one minor change to reduce the dependency count. Additionally, it does not automatically apply the Catalyst::Request::REST action class to your global Catalyst action class. This feature is left off because its easy to set this yourself if desired via the global Catalyst configuration and we want to follow and promote the idea of 'do one thing well and nothing surprising'.

NOTE There is an additional minor change in how we handle return values from actions. In general Catalyst does nothing with an action return value (unless in an auto action). However this might not always be the future case, and you might have used that return value for something in your custom code. In Catalyst::Action::REST the return value was always the return of the dispatched sub action (if any). We tweaked this so that we use the sub action return value, BUT if that value is undefined, we use the parent action return value instead.

We also dropped saying 'REST' when all we are doing is dispatching on HTTP method. Since the time that the first version of Catalysts::Action::REST was released to CPAN our notion of what 'REST' means has greatly evolved so I think its correct to change the name to be functionality specific and to not confuse people that are new to the REST discipline.

This action role is intended to be used in all the places you used to use the action class and have the same results, with the exception of the already mentioned 'not messing with the global request class'. However Catalyst::Action::REST has been around for a long time and is well vetted in production so I would caution care with changing your mission critical systems very quickly.

VERSUS NATIVE METHOD ATTRIBUTES

Catalyst since version 5.90030 has offered a core approach to dispatch on the http method (via Catalyst::ActionRole::HTTPMethods). Why still use this action role versus the core functionality? ALthough it partly comes down to preference and the author's desire to give current users of Catalyst::Action::REST a path forward, there is some functionality differences beetween the two which may recommend one over the other. For example the core method matching does not offer an automatic default 'Not Implemented' response that correctly sets the OPTIONS header. Also the dispatch flow between the two approaches is different and when using chained actions one might be a better choice over the other depending on how your chains are arranged and your desired flow of action.

METHODS

This role contains the following methods.

get_allowed_methods

Returns a list of the allowed methods.

dispatch

This method overrides the default dispatch mechanism to the re-dispatching mechanism described above.

CONTRIBUTORS

This module is based on code, tests and documentation extracted out of Catalyst::Action::REST, which was originally developed by Adam Jacob with lots of help from mst and jrockway, while being paid by Marchex, Inc (http://www.marchex.com).

The following people also contributed to parts copied from that package:

Tomas Doran (t0m) <bobtfish@bobtfish.net>

Dave Rolsky <autarch@urth.org>

Arthur Axel "fREW" Schmidt <frioux@gmail.com>

J. Shirley <jshirley@gmail.com>

Wallace Reis <wreis@cpan.org>

AUTHOR

Aristotle Pagaltzis <pagaltzis@gmx.de>

John Napiorkowski <jjnapiork@cpan.org>

COPYRIGHT AND LICENSE

This software is copyright (c) 2024 by Aristotle Pagaltzis.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.