File Coverage

blib/lib/Catalyst/Plugin/Errors.pm
Criterion Covered Total %
statement 18 77 23.3
branch 0 26 0.0
condition 0 10 0.0
subroutine 6 13 46.1
pod 3 7 42.8
total 27 133 20.3


line stmt bran cond sub pod time code
1              
2             use Moose;
3 1     1   1092 use MRO::Compat;
  1         2  
  1         14  
4 1     1   5314 use CatalystX::Utils::ContentNegotiation;
  1         2  
  1         20  
5 1     1   4 use CatalystX::Utils::ErrorMessages;
  1         2  
  1         28  
6 1     1   5 use Catalyst::Utils;
  1         2  
  1         17  
7 1     1   4 use Scalar::Util ();
  1         2  
  1         14  
8 1     1   4  
  1         7  
  1         1101  
9             our %DEFAULT_ERROR_VIEWS = (
10             'text/html' => 'Errors::HTML',
11             'text/plain' => 'Errors::Text',
12             'application/json' => 'Errors::JSON',
13             );
14              
15             my %views = %DEFAULT_ERROR_VIEWS;
16             my @accepted = ();
17             my $default_media_type = 'text/plain';
18             my $default_language = 'en_US';
19              
20             my $profile = sub {
21             my $c = shift;
22             $c->stats->profile(@_)
23             if $c->debug;
24             };
25              
26             my $available_languages = sub {
27             my ($c) = @_;
28             return my @lang_tags = CatalystX::Utils::ErrorMessages::available_languages;
29             };
30              
31             my $get_language = sub {
32             my ($c) = @_;
33             if(my $lang = $c->request->header('Accept-Language')) {
34             return CatalystX::Utils::ContentNegotiation::content_negotiator->choose_language([$c->$available_languages], $lang) || $default_language;
35             }
36             return $default_language;
37             };
38              
39             my $get_message_info = sub {
40             my ($c, $lang, $code) = @_;
41             return my $message_info_hash = CatalystX::Utils::ErrorMessages::get_message_info($lang, $code);
42             };
43              
44             my $finalize_message_info = sub {
45             my ($c, $code, $lang, %args) = @_;
46             my $message_info = $c->$get_message_info($lang, $code);
47             $message_info = $c->$get_message_info('en_US', $code) unless $message_info; # Fallback to US English
48             return (
49             %$message_info,
50             lang => $lang,
51             %args,
52             );
53             };
54              
55             my ($c, $code, %args) = @_;
56             return 'http_error';
57 0     0 0   }
58 0            
59             my ($c, $code, %args) = @_;
60             my $lang = $c->$get_language;
61             my %message_info = $c->$finalize_message_info($code, $lang, %args);
62 0     0 1   return (
63 0           status_code => $code,
64 0           uri => "@{[ $c->req->uri ]}",
65             %message_info );
66 0           }
67 0            
68             my ($self, $obj) = @_;
69             return Scalar::Util::blessed($obj) && $obj->can('as_http_response') ? 1:0;
70             }
71              
72 0     0 0   my $app = shift;
73 0 0 0       my $ret = $app->maybe::next::method(@_);
74             my $config = $app->config->{'Plugin::Errors'};
75              
76             %views = (%views, %{$config->{views}}) if $config->{views};
77 0     0 0   $default_media_type = $config->{default_media_type} if exists $config->{default_media_type};
78 0           $default_language = $config->{default_language} if exists $config->{default_language};
79 0            
80             @accepted = keys %views;
81 0 0          
  0            
82 0 0         return $ret;
83 0 0         }
84              
85 0           my ($app, @args) = @_;
86             my $ret = $app->maybe::next::method(@_);
87 0            
88             my $namespace = "${app}::View";
89             my %views_we_have = map { Catalyst::Utils::class2classsuffix($_) => 1 }
90             grep { m/$namespace/ }
91 0     0 0   keys %{ $app->components };
92 0            
93             foreach my $view_needed (values %views) {
94 0           next if $views_we_have{"View::${view_needed}"};
95 0           $app->log->debug("Injecting Catalyst::View::${view_needed}") if $app->debug;
96 0           Catalyst::Utils::ensure_class_loaded("Catalyst::View::${view_needed}");
97 0           Catalyst::Utils::inject_component(
  0            
98             into => $app,
99 0           component => "Catalyst::View::${view_needed}",
100 0 0         as => $view_needed );
101 0 0         }
102 0            
103 0           return $ret;
104             }
105              
106             my ($c, $code, @args) = @_;
107             my (@additional_headers, %data) = ();
108              
109 0           @additional_headers = @{shift(@args)} if (ref($args[0])||'') eq 'ARRAY';
110             while(@additional_headers) {
111             $c->response->headers->header(shift(@additional_headers), shift(@additional_headers));
112             }
113 0     0 1    
114 0           %data = %{shift(@args)} if (ref($args[0])||'') eq 'HASH';
115             %data = $c->finalize_error_args($code, %data);
116 0 0 0        
  0            
117 0           my $chosen_media_type = CatalystX::Utils::ContentNegotiation::content_negotiator
118 0           ->choose_media_type(\@accepted, $c->request->header('Accept'))
119             || $default_media_type;
120              
121 0 0 0       $c->log->debug("Error dispatched to mediatype '$chosen_media_type' using view '$views{$chosen_media_type}'") if $c->debug;
  0            
122 0           $c->log->error($data{error}) if exists($data{error});
123              
124 0   0       my $chosen_view = $views{$chosen_media_type};
125             my $view_class = ref($c) . "::View::${chosen_view}";
126             my $view_obj = $view_class->can('ACCEPT_CONTEXT') ?
127             $c->view($chosen_view, %data) :
128 0 0         $c->view($chosen_view);
129 0 0          
130             if(my $sub = $view_obj->can("http_${code}")) {
131 0           $c->$profile(begin => "=> View::${chosen_view}");
132 0           $view_obj->$sub($c, %data);
133 0 0         $c->$profile(end => "=> View::${chosen_view}");
134             } elsif($view_obj->can('http_default')) {
135             $c->$profile(begin => "=> View::${chosen_view}");
136             $view_obj->http_default($c, $code, %data);
137 0 0         $c->$profile(end => "=> View::${chosen_view}");
    0          
138 0           } else {
139 0           my $template = $c->generate_error_template_name($code, %data);
140 0           $c->stash(template=>$template, %data);
141             $c->forward($view_obj);
142 0           }
143 0           }
144 0            
145             my $c = shift;
146 0           $c->dispatch_error(@_);
147 0           $c->detach;
148 0           }
149              
150             __PACKAGE__->meta->make_immutable;
151              
152             =head1 NAME
153 0     0 1    
154 0           Catalyst::Plugin::Errors - Standard error responses with content negotiation
155 0            
156             =head1 SYNOPSIS
157              
158             Use in your application class
159              
160             package Example;
161              
162             use Catalyst;
163              
164             __PACKAGE__->setup_plugins([qw/Errors/]);
165             __PACKAGE__->setup();
166             __PACKAGE__->meta->make_immutable();
167              
168             And then you can use it in a controller (or anyplace where you have C<$c> context).
169              
170             package Example::Controller::Root;
171              
172             use Moose;
173             use MooseX::MethodAttributes;
174              
175             extends 'Catalyst::Controller';
176              
177             sub root :Chained(/) PathPart('') CaptureArgs(0) {}
178              
179             sub not_found :Chained(root) PathPart('') Args {
180             my ($self, $c, @args) = @_;
181             $c->detach_error(404);
182             }
183              
184             __PACKAGE__->config(namespace=>'');
185             __PACKAGE__->meta->make_immutable;
186              
187             =head1 DESCRIPTION
188              
189             This is a plugin which installs (if needed) View classes to handle HTTP errors (4xx
190             and 5xx codes) in a regular and content negotiated way. See <CatalystX::Errors>
191             for a high level overview. Documentation here is more API level and the examples
192             are sparse.
193              
194             =head1 METHODS
195              
196             This plugin adds the following methods to your C<$c> context.
197              
198             =head2 dispatch_error ($code, ?\@additional_headers, ?\%template_args)
199              
200             Examples:
201              
202             $c->detach_error(404);
203             $c->detach_error(404, +{error=>'invalid uri request'});
204             $c->detach_error(401, ['WWW-Authenticate" => 'Basic realm=myapp, charset="UTF-8"'], +{error=>'unauthorized access attempt'});
205              
206             Dispatches to an error view based on content negotiation and the provided code. You can also pass
207             an arrayref of extra HTTP headers (such as www-authenticate for 401 errors) and also optionally
208             a hashref of fields that will be sent to the view.
209              
210             When dispatching to a C<$view> we use the following rules in order:
211              
212             First if the View has a method C<http_${code}> (where C<$code> is the HTTP status code you are
213             using for the error) we call that method with args C<$c, %template_args> and expect that method to setup
214             a valid error response.
215              
216             Second, call the method C<http_default> with args C<$c, $code, %template_args> if that exists.
217              
218             If neither method exists we call C<$c->forward($view)> and C<%template_args> are added to the stash, along
219             with a stash var 'template' which is set to 'http_error'. This should work with most standard L<Catalyst>
220             views that look at the stash field 'template' to find a template name. If you prefer a different template
221             name you can override the method 'generate_error_template_name' to make it whatever you wish.
222              
223             B<NOTE> Using C<dispatch_error> (or C<detach_error>) doesn't add anything to the Catalyst error log
224             as we consider this control flow more than anything else. If you want to log a special line you can
225             add an C<error> field to C<%template_args> and that we go to the error log.
226              
227             =head2 detach_error
228              
229             Calls L</dispatch_error> with the provided arguments and then does a C<$c->detach> which
230             effectively ends processing for the action.
231              
232             =head1 CONFIGURATION & CUSTOMIZATION
233              
234             This plugin can be customized with the following configuration options or via
235             overriding or adapting the following methods
236              
237             =head2 finalize_error_args
238              
239             This method provides the actual arguments given to the error view (args which are for
240             example used in the template for messaging to the end user). You can override this
241             to provide your own version. See the source for how this should work
242              
243             =head2 Configuration keys
244              
245             This plugin defines the following configuration by default, which you can override.
246              
247             package Example;
248              
249             use Catalyst;
250              
251             __PACKAGE__->setup_plugins([qw/Errors/]);
252             __PACKAGE__->config(
253             # This is the configuration which is default. You don't have to actually type
254             # this out. I'm just putting it here to show you what its doing under the hood.
255             'Plugin::Errors' => +{
256             default_media_type => 'text/plain',
257             default_language => 'en_US',
258             views => +{
259             'text/html' => 'Errors::HTML',
260             'text/plain' => 'Errors::Text',
261             'application/json' => 'Errors::JSON',
262             },
263             },
264             );
265              
266             __PACKAGE__->setup();
267             __PACKAGE__->meta->make_immutable();
268              
269             By default we map the media types C<text/html>, C<text/plain> and C<application/json> to
270             cooresponding views. This views are injected automatically if you don't provide subclasses
271             or your own view locally. The following views are injected as needed:
272              
273             L<Catalyst::View::Error::HTML>, L<Catalyst::View::Error::Text>, and L<L<Catalyst::View::Error::JSON>.
274              
275             You can check the docs for each of the default views for customization options but you can always
276             make a local subclass inside you application's view directory and tweak as desired (or you can just
277             use your own view or one of the common ones on CPAN).
278              
279             You can also add additional media types mappings.
280              
281             =head1 SEE ALSO
282            
283             L<CatalystX::Errors>.
284              
285             =head1 AUTHOR
286            
287             L<CatalystX::Errors>.
288            
289             =head1 COPYRIGHT & LICENSE
290            
291             L<CatalystX::Errors>.
292              
293             =cut