2016-09-15 3 views
3

Мне сложно найти способ обработки неожиданных ошибок в моем API-интерфейсе Catalyst::Controller::REST.Какова наилучшая практика для обработки ошибок с Catalyst :: Controller :: REST

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

ответ

3

Это не лучшая практика. Я просто сделаю это.

Вы можете использовать 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. К ним относятся:

Вы можете реализовать свой собственный отсутствующего статус по подклассов REST Catalyst :: Контролера :: или путем добавления в его пространство имен , 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. Он позволяет определить метод 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::Class или 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; 
} 

Опять же, было бы целесообразно переместить исключения в их собственный модуль, чтобы вы могли использовать их повсюду.

Это может быть хорошо продлен, я считаю. Также имейте в виду, что соединение бизнес-логики или моделей слишком сильно связано с тем, что это веб-приложение - плохой дизайн. Я выбрал очень известные имена исключений, потому что это легко объяснить. Возможно, вам захочется использовать более общие или, скорее, менее ориентированные на веб-имена имена, и ваша диспетчеризация должна фактически отображать их. В противном случае он слишком привязан к веб-слою.


1) Да, это множественное число. См. here.

+1

Отличный ответ, спасибо за то, что вы так тщательно. Я посмотрел на источник «Catalyst :: ControllerRole :: CatchErrors», и они используют 'before 'end' => sub {...}', что именно то, что я искал. Единственная проблема заключается в том, что объект '@ error' завершается в строку, такую ​​как' Caught exception в App :: API :: Foo-> default ... ', вероятно, добавленный классом контроллера REST, поэтому исключения не могут быть легко отправляется. – ojosilva

+0

@ojosilva hmm, это плохой. Нет ли там реального объекта? Или ты просто «умер»? – simbabque

+0

Моя модель, которая существует от пути до этого нового REST api, над которым я работаю, использует 'die' обильно. Использование действия «Try :: Tiny» по действию не является хорошим вариантом, учитывая огромное количество действий, но это может быть единственный способ избежать того, чтобы внутренняя ошибка модели была завершена во внешнюю строку сообщения. – ojosilva