This is not a best practice. It's just how I would do it.
You can use Try::Tiny to catch the errors in your controller, and the helpers that Catalyst::Action::REST brings to send the appropriate response codes. It will take care of converting the response to the right format (i.e. JSON) for you.
But that still requires you to do it for each type of error. Basically it boils down to this:
use Try::Tiny;
BEGIN { extends 'Catalyst::Controller::REST' }
__PACKAGE__->config(
json_options => { relaxed => 1, allow_nonref => 1 },
default => 'application/json',
map => { 'application/json' => [qw(View JSON)] },
);
sub default : Path : ActionClass('REST') { }
sub default_GET {
my ( $self, $c, $mid ) = @_;
try {
# ... (there might be a $c->detach in here)
} catch {
# this is thrown by $c->detach(), so don't 400 in this case
return if $_->$_isa('Catalyst::Exception::Detach');
$self->status_bad_request( $c, message => q{Boom!} );
}
}
The methods for those kinds of responses are listed in Catalyst::Controller::REST under STATUS HELPERS. They are:
You can implement your own for the missing status1 by subclassing Catalyst::Controller::REST or by adding into its namespace. Refer to one of them for how they are constructed. Here's an example.
*Catalyst::Controller::REST::status_teapot = sub {
my $self = shift;
my $c = shift;
my %p = Params::Validate::validate( @_, { message => { type => SCALAR }, }, );
$c->response->status(418);
$c->log->debug( "Status I'm A Teapot: " . $p{'message'} ) if $c->debug;
$self->_set_entity( $c, { error => $p{'message'} } );
return 1;
}
If that is too tedious because you have a lot of actions, I suggest you make use of the end
action like you intended. More to how that works a bit further below.
In that case, don't add the Try::Tiny construct to your actions. Instead, make sure that all of your models or other modules you are using throw good exceptions. Create exception classes for each of the cases, and hand the control of what is supposed to happen in which case to them.
A good way to do all this is to use Catalyst::ControllerRole::CatchErrors. It lets you define a catch_error
method that will handle errors for you. In that method, you build a dispatch table that knows what exception should cause which kind of response. Also look at the documentation of $c->error
as it has some valuable information here.
package MyApp::Controller::Root;
use Moose;
use Safe::Isa;
BEGIN { extends 'Catalyst::Controller::REST' }
with 'Catalyst::ControllerRole::CatchErrors';
__PACKAGE__->config(
json_options => { relaxed => 1, allow_nonref => 1 },
default => 'application/json',
map => { 'application/json' => [qw(View JSON)] },
);
sub default : Path : ActionClass('REST') { }
sub default_GET {
my ( $self, $c, $mid ) = @_;
$c->model('Foo')->frobnicate;
}
sub catch_errors : Private {
my ($self, $c, @errors) = @_;
# Build a callback for each of the exceptions.
# This might go as an attribute on $c in MyApp::Catalyst as well.
my %dispatch = (
'MyApp::Exception::BadRequest' => sub {
$c->status_bad_request(message => $_[0]->message);
},
'MyApp::Exception::Teapot' => sub {
$c->status_teapot;
},
);
# @errors is like $c->error
my $e = shift @errors;
# this might be a bit more elaborate
if (ref $e =~ /^MyAPP::Exception/) {
$dispatch{ref $e}->($e) if exists $dispatch{ref $e};
$c->detach;
}
# if not, rethrow or re-die (simplified)
die $e;
}
The above is a crude, untested example. It might not work like this exactly, but it's a good start. It would make sense to move the dispatching into an attribute of your main Catalyst application object (the context, $c
). Place it in MyApp::Catalyst to do that.
package MyApp::Catalyst;
# ...
has error_dispatch_table => (
is => 'ro',
isa => 'HashRef',
traits => 'Hash',
handles => {
can_dispatch_error => 'exists',
dispatch_error => 'get',
},
builder => '_build_error_dispatch_table',
);
sub _build_error_dispatch_table {
return {
'MyApp::Exception::BadRequest' => sub {
$c->status_bad_request(message => $_[0]->message);
},
'MyApp::Exception::Teapot' => sub {
$c->status_teapot;
},
};
}
And then do the dispatching like this:
$c->dispatch_error(ref $e)->($e) if $c->can_dispatch_error(ref $e);
Now all you need is good exceptions. There are different ways to do those. I like Exception::Class or Throwable::Factory.
package MyApp::Model::Foo;
use Moose;
BEGIN { extends 'Catalyst::Model' };
# this would go in its own file for reusability
use Exception::Class (
'MyApp::Exception::Base',
'MyApp::Exception::BadRequest' => {
isa => 'MyApp::Exception::Base',
description => 'This is a 400',
fields => [ 'message' ],
},
'MyApp::Exception::Teapot' => {
isa => 'MyApp::Exception::Base',
description => 'I do not like coffee',
},
);
sub frobnicate {
my ($self) = @_;
MyApp::Exception::Teapot->throw;
}
Again, it would make sense to move the exceptions into their own module so you can reuse them everywhere.
This can be nicely extended I believe.
Also keep in mind that coupling the business logic or models too strongly to the fact that it's a web app is be bad design. I chose very speaking exception names because it's easy to explain that way. You might want to just more generic or rather less web centric names, and your dispatch thingy should take of actually mapping them. Otherwise it's too much tied to the web layer.
1) Yes, this is the plural. See here.