Perl 什么';这是使用Catalyst::Controller::REST处理错误的最佳实践

Perl 什么';这是使用Catalyst::Controller::REST处理错误的最佳实践,perl,catalyst,Perl,Catalyst,我很难找到一种方法来处理基于Catalyst::Controller::REST的API中的意外错误 BEGIN { extends 'Catalyst::Controller::REST' } __PACKAGE__->config( json_options => { relaxed => 1, allow_nonref => 1 }, default => 'application/json', map =

我很难找到一种方法来处理基于
Catalyst::Controller::REST
的API中的意外错误

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 ) = @_;

   ### something happens here and it dies
}
如果
default\u GET
意外死亡,将显示应用程序标准状态500错误页面。我希望控制器后面的REST库能够控制它并显示JSON错误(或者REST请求接受的任何序列化响应)

逐个操作添加错误控制(使用ie
Try::Tiny
)不是一个选项。我希望集中所有的错误处理。我尝试过使用
子端
操作,但没有成功

sub error :Private {
    my ( $self, $c, $code, $reason ) = @_;

    $reason ||= 'Unknown Error';
    $code ||= 500;

    $c->res->status($code);

    $c->stash->{data} = { error => $reason };
}

这不是最佳做法。我就是这么做的

您可以使用捕获控制器中的错误,以及Catalyst::Action::REST带来的帮助程序,以发送适当的响应代码。它将负责为您将响应转换为正确的格式(即JSON)

但这仍然要求您针对每种类型的错误执行此操作。基本上可以归结为:

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!} );
    }
}
中列出了此类响应的方法。它们是:

您可以通过子类化Catalyst::Controller::REST或将其添加到名称空间中,为缺少的status1实现自己的。它们是如何构造的。这里有一个例子

*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;
}

如果你有很多动作,这太单调了,我建议你按照你的意愿使用
end
动作。下面我们将进一步介绍这一点的工作原理

在这种情况下,不要将Try::Tiny构造添加到操作中。相反,请确保您使用的所有模型或其他模块都会抛出良好的异常。为每种情况创建异常类,并将在哪种情况下应该发生的事情的控制权交给他们

做这一切的一个好方法是使用。它允许您定义一个
catch\u error
方法来为您处理错误。在该方法中,您构建了一个调度表,该表知道什么异常应该引起哪种响应。另外,请查看,因为这里有一些有价值的信息

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;
}
以上是一个粗糙的、未经测试的例子。它可能不完全像这样工作,但这是一个好的开始。将调度移到主Catalyst应用程序对象(上下文,
$c
)的属性中是有意义的。将其放在MyApp::Catalyst中即可

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; 
         },
    };
}
然后像这样进行调度:

$c->dispatch_error(ref $e)->($e) if $c->can_dispatch_error(ref $e);
现在你只需要好的例外。有不同的方法可以做到这一点。我喜欢或喜欢

同样,将异常移动到它们自己的模块中是有意义的,这样您就可以在任何地方重用它们

我相信这可以很好地扩展。 还要记住,将业务逻辑或模型与它是一个web应用这一事实过于紧密地耦合是一种糟糕的设计。我选择了非常会说话的异常名称,因为这样很容易解释。您可能只需要更通用的名称,或者更少以web为中心的名称,您的dispatch thingy应该负责实际映射它们。否则,它与web层的关联太多



1) 是的,这是复数形式。请参阅。

回答得很好,谢谢你这么彻底。我已经查看了Catalyst::ControllerRole::CatchErrors源代码,它们在'end'=>子{…}之前使用了
,这正是我要查找的。唯一的问题是,
@error
对象被包装成一个字符串,例如App::API::Foo->default中的
捕获异常…
可能是由REST控制器类添加的,因此异常无法轻松调度。@ojosilva-hmm,这很糟糕。那里没有实物吗?或者您刚刚使用了
die
?我的模型在我正在开发的这个新RESTAPI之前就已经存在了,它大量使用
die
。考虑到操作的数量,使用
Try::Tiny
action by action不是一个好的选择,但这可能是避免将内部模型错误包装到外部消息字符串中的唯一方法。@ojo Catalyst将错误消息包装到对象中。您可能需要按照我的建议将模式匹配构建到dispa中。如果您没有自己的错误对象可以使用ref,那么只需使用消息即可。您是对的。如果我扔了一个物体,它会干净地出来。问题在于,从这些模型中抛出的错误以及意外错误都是字符串。
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;
}