【问题标题】:What's the best practice for error handling with Catalyst::Controller::REST使用 Catalyst::Controller::REST 处理错误的最佳实践是什么
【发布时间】:2016-09-15 19:31:28
【问题描述】:

我很难找到一种方法来处理基于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_GET 意外死亡,则会显示应用程序标准状态 500 错误页面。我希望控制器后面的 REST 库能够控制它并显示 JSON 错误(或 REST 请求接受的任何序列化响应)。

逐个动作添加错误控制(即Try::Tiny)不是一种选择。我希望集中所有错误处理。我尝试过使用sub end 操作,但没有奏效。

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

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

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

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

【问题讨论】:

    标签: perl catalyst


    【解决方案1】:

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

    您可以使用Try::Tiny 来捕获控制器中的错误,以及 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 under STATUS HELPERS 中列出。它们是:

    您可以通过继承 Catalyst::Controller::REST 或添加到其命名空间来实现自己的缺失状态1Refer to one of them 了解它们的构造方式。这是一个例子。

    *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 构造添加到您的操作中。相反,请确保您使用的所有模型或其他模块都抛出良好的异常。为每种情况创建异常类,并将在这种情况下应该发生的事情的控制权交给它们。

    完成这一切的一个好方法是使用Catalyst::ControllerRole::CatchErrors。它允许您定义一个 catch_error 方法来为您处理错误。在该方法中,您构建了一个调度表,该表知道哪个异常应该导致哪种响应。还请查看documentation of $c->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);
    

    现在您所需要的只是好的例外。有不同的方法可以做到这些。我喜欢Exception::ClassThrowable::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;
    }
    

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

    我相信这可以很好地扩展。 还要记住,将业务逻辑或模型与它是一个 Web 应用程序这一事实过度耦合是不好的设计。我选择了非常有说服力的异常名称,因为这样很容易解释。您可能想要更通用或更不以网络为中心的名称,并且您的调度应该采取实际映射它们的方式。否则它与 web 层的联系太紧密了。


    1) 是的,这是复数形式。见here

    【讨论】:

    • 优秀的答案,感谢您如此彻底。我查看了Catalyst::ControllerRole::CatchErrors 源,他们使用before 'end' => sub { ... },这正是我想要的。唯一的问题是 @error 对象被包装成一个字符串,例如 Caught exception in App::API::Foo->default ... 可能是由 REST 控制器类添加的,因此无法轻松分派异常。
    • @ojosilva 嗯,这很糟糕。那里没有实物吗?还是你只是die
    • 我的模型在我正在研究的这个新的 REST api 之前就存在,大量使用die。考虑到操作的绝对数量,逐个操作使用Try::Tiny 并不是一个好的选择,但它可能是避免将内部模型错误包装到外部消息字符串中的唯一方法。
    • @ojo Catalyst 将您的错误消息包装在一个对象中。您可能需要按照我的建议将模式匹配构建到 dispa 中。如果您没有自己的错误对象,您可以在其中使用 ref,那么只需使用消息即可。
    • 你是对的。如果我扔一个物体,它就会干净。问题是从这些模型中抛出的错误以及意外错误都是字符串。
    猜你喜欢
    • 2021-07-25
    • 2011-09-22
    • 2020-06-05
    • 1970-01-01
    • 2019-10-03
    • 2019-07-12
    • 2020-12-24
    • 2016-12-26
    • 1970-01-01
    相关资源
    最近更新 更多