使用 Catalyst::Controller::REST 进行错误处理的最佳做法是什么
What's the best practice for error handling with Catalyst::Controller::REST
我很难找到一种方法来处理基于 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 };
}
这不是最佳做法。这就是我会怎么做的。
您可以使用 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 中列出了这些响应的方法。他们是:
status_ok
status_created
status_accepted
status_no_content
status_multiple_choices
status_found
status_bad_request
status_forbidden
status_not_found
gone
status_see_other
status_moved
您可以通过子类化 Catalyst::Controller::REST 或添加到其名称空间来实现您自己的缺失状态 1。 Refer 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. 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
,因为这里有一些有价值的信息。
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::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;
}
同样,将异常移动到它们自己的模块中是有意义的,这样您就可以在任何地方重用它们。
我相信这可以很好地扩展。
还要记住,将业务逻辑或模型与它是 Web 应用程序这一事实的耦合过于强烈是一种糟糕的设计。我选择了非常通俗易懂的异常名称,因为这样很容易解释。您可能想要更通用或更少以网络为中心的名称,并且您的调度应该实际映射它们。否则它与 web 层的联系太多了。
1) 是的,这是复数。见 here.
我很难找到一种方法来处理基于 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 };
}
这不是最佳做法。这就是我会怎么做的。
您可以使用 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 中列出了这些响应的方法。他们是:
status_ok
status_created
status_accepted
status_no_content
status_multiple_choices
status_found
status_bad_request
status_forbidden
status_not_found
gone
status_see_other
status_moved
您可以通过子类化 Catalyst::Controller::REST 或添加到其名称空间来实现您自己的缺失状态 1。 Refer 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. 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
,因为这里有一些有价值的信息。
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::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;
}
同样,将异常移动到它们自己的模块中是有意义的,这样您就可以在任何地方重用它们。
我相信这可以很好地扩展。 还要记住,将业务逻辑或模型与它是 Web 应用程序这一事实的耦合过于强烈是一种糟糕的设计。我选择了非常通俗易懂的异常名称,因为这样很容易解释。您可能想要更通用或更少以网络为中心的名称,并且您的调度应该实际映射它们。否则它与 web 层的联系太多了。
1) 是的,这是复数。见 here.