File Coverage

lib/OAuthomatic/Error.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1 1     1   908 use strict;
  1         2  
  1         53  
2 1     1   7 use warnings;
  1         2  
  1         64  
3              
4             ## no critic (ProhibitMultiplePackages, RequireFilenameMatchesPackage)
5              
6             =head1 NAME
7              
8             OAuthomatic::Error - structured exceptions thrown by OAuthomatic
9              
10             =head1 DESCRIPTION
11              
12             Errors defined here allow for inspection of various error details.
13              
14             =head1 SYNOPSIS
15              
16             try {
17             OAuthomatic::Error::Sth->throw({
18             ident => 'short description',
19             # ... other params
20             });
21             } catch {
22             my $error = $_;
23             if ($error->isa('OAuthomatic::Error')) {
24             print $error->message, "\n\n", $error->stack_trace->as_string;
25             # Or use class-dependant fields
26             }
27             };
28              
29             =cut
30              
31             {
32             package OAuthomatic::Error;
33 1     1   268 use Moose;
  0            
  0            
34             use Moose::Util::TypeConstraints;
35             sub Payload { 'Role::HasPayload::Meta::Attribute::Payload' }
36             use namespace::sweep;
37              
38             # Some, but not all, parts of Throwable::X
39             with 'Throwable'; # ->throw
40             with 'Role::HasPayload::Merged'; # ->payload (marked attribs + explicit payload)
41             with 'StackTrace::Auto'; # ->stack_trace->as_string
42             # Subtypes use Role::HasMessage::Errf
43              
44             has ident => (is => 'ro', required => 1,
45             isa => subtype('Str', where { length && /\A\S/ && /\S\z/ }),
46             traits => [Payload]);
47              
48             use overload fallback => 1,
49             '""' => sub { $_[0]->message };
50             };
51              
52             =head2 OAuthomatic::Error::HTTPFailure
53              
54             Object representing various communication and OAuth-protocol related failures.
55              
56             try {
57             OAuthomatic::Error::HTTPFailure->throw({
58             ident => 'OAuth HTTP request failed',
59             request => $request, # HTTP::Request
60             response => $response, # HTTP::Response
61             });
62             } catch {
63             my $error = $_;
64             if ($error->isa('OAuthomatic::Error::HTTPFailure')) {
65             print "$error\n"; # message
66             print $error->stack_trace->as_string; # if necessary
67             if($error->is_new_client_key_required) {
68             # request new client (application) key
69             } elsif($error->is_new_token_required) {
70             # redo authorization sequence
71             }
72             # See also other fields - code, uri, struct_detail
73             }
74             };
75              
76             =head3 METHODS
77              
78             =over 4
79              
80             =item C<is_new_client_key_required()>
81              
82             Do details of this error mean, that OAuth client key in use is no longer valid and should be replaced?
83              
84             =item C<is_new_token_required()>
85              
86             Do details of this error mean, that OAuth token in use is no longer valid and application
87             should get new one?
88              
89             =back
90              
91             =head3 ATTRIBUTES
92              
93             =over 4
94              
95             =item C<request>
96              
97             http::request object containing request which caused failure
98              
99             =item C<response>
100              
101             HTTP::Response object containing obtained reply
102              
103             =item C<code>
104              
105             Shortcut. HTTP error code (400, 401, 500, ...).
106              
107             =item C<status>
108              
109             Shortcut. HTTP status line
110              
111             =item C<oauth_problem>
112              
113             If description of actual OAuth problem was detected, appropriate text code, for
114             example C<parameter_absent>, C<token_revoked>, C<consumer_key_rejected>, ...
115              
116             See L<http://wiki.oauth.net/w/page/12238543/ProblemReporting> for possible values.
117              
118             =item C<detail>
119              
120             Error detail. Formatted from information available in response content (if format
121             was not recognized, this very content by itself).
122              
123             =item C<struct_detail>
124              
125             Deserialized error detail in case output contains form-encoded data. Handles:
126              
127             =over 4
128              
129             =item form-serialized data
130              
131             Frequently used in OAuth initial protocol sequences, for example you may see here:
132              
133             {
134             oauth_problem => 'parameter_absent',
135             oauth_parameters_absent => 'oauth_consumer_key',
136             }
137              
138             =item JSON error output
139              
140             For example
141              
142             {
143             error => { id => '9e9c7bddeff3',
144             message => 'Object already deleted' },
145             }
146              
147             =back
148              
149             =item C<method>
150              
151             Shortcut. HTTP method (GET, POST, PUT, DELETE)
152              
153             =item C<uri>
154              
155             Shortcut. URI object representing the call.
156              
157             =back
158              
159             =cut
160              
161             {
162             package OAuthomatic::Error::HTTPFailure;
163             use Moose;
164             use Try::Tiny;
165             use OAuthomatic::Internal::Util;
166             use Data::Dump qw(dump);
167             use namespace::sweep;
168              
169             extends 'OAuthomatic::Error';
170             with 'Role::HasMessage::Errf' => {
171             default => "OAuthomatic HTTP failure: %{ident}s.\n"
172             . " Code: %{code}s. Status: %{status}s\n"
173             . " Call: %{method}s %{uri}s\n"
174             . " %{detail}s",
175             };
176              
177             sub Payload { 'Role::HasPayload::Meta::Attribute::Payload' }
178              
179             has request => (is => 'ro', isa => 'HTTP::Request', required => 1);
180             has response => (is => 'ro', isa => 'HTTP::Response', required => 1);
181              
182             has code => (is => 'ro', lazy_build => 1, traits => [Payload]);
183             has status => (is => 'ro', lazy_build => 1, traits => [Payload]);
184             has method => (is => 'ro', lazy_build => 1, traits => [Payload]);
185             has uri => (is => 'ro', lazy_build => 1, traits => [Payload]);
186             has detail => (is => 'ro', lazy_build => 1, traits => [Payload]);
187             # In some cases we get form-encoded error attributes, if they
188             # are present, we keep them there
189             has struct_detail => (is => 'ro', lazy_build => 1);
190             # Detailed info about problem, if any, http://wiki.oauth.net/w/page/12238543/ProblemReporting
191             has oauth_problem => (is => 'ro', lazy_build => 1);
192              
193             sub is_new_client_key_required {
194             my $self = shift;
195             my $problem = $self->oauth_problem || '';
196             if($problem =~ /^(consumer_key_unknown|consumer_key_rejected)$/x) {
197             return 1;
198             }
199             return 0;
200             }
201              
202             sub is_new_token_required {
203             my $self = shift;
204             my $problem = $self->oauth_problem || '';
205             if($problem =~ /^(token_expired|token_revoked|token_rejected|permission_unknown|permission_denied)$/x) {
206             return 1;
207             }
208             return 0;
209             }
210              
211             sub _build_code {
212             return $_[0]->response->code;
213             }
214             sub _build_status {
215             return $_[0]->response->message;
216             }
217             sub _build_method {
218             return $_[0]->request->method;
219             }
220             sub _build_uri {
221             return $_[0]->request->uri;
222             }
223             sub _build_struct_detail {
224             my $self = shift;
225             my $reply;
226              
227             my $response = $self->response;
228             return unless $response;
229              
230             my ($content_type, $encoding) = $response->content_type;
231              
232             # HTML form errors. Some real examples:
233             # (in headers)
234             # Content-Type: application/x-www-form-urlencoded;charset=UTF-8
235             # (in body)
236             # oauth_parameters_absent=oauth_consumer_key%26oauth_signature_method%26oauth_signature%26oauth_timestamp&oauth_problem=parameter_absent
237             # (or)
238             # oauth_parameters_absent=oauth_consumer_key&oauth_problem=parameter_absent
239             if($content_type eq 'application/x-www-form-urlencoded') {
240             try {
241             $reply = parse_httpmsg_form($response, 1);
242             };
243             }
244             elsif($content_type =~ m{^application/(?:x-)?json}x) {
245             try {
246             $reply = parse_httpmsg_json($response);
247             };
248             }
249              
250             # FIXME: maybe compact JSON level up if it contains just 'error'
251              
252             # FIXME: XML errors (LinkedIn for example)
253              
254             return $reply;
255             }
256              
257             sub _build_oauth_problem {
258             my $self = shift;
259             my $struct_detail = $self->struct_detail;
260             if($struct_detail) {
261             if(exists $struct_detail->{oauth_problem}) {
262             return $struct_detail->{oauth_problem};
263             }
264             }
265             return ''; # To make comparisons easier
266             }
267              
268             sub _build_detail {
269             my $self = shift;
270              
271             my $struct_detail = $self->struct_detail;
272             my $detail_text;
273             if($struct_detail) {
274             local $Data::Dump::INDENT = " ";
275             $detail_text = dump($struct_detail);
276             chomp($detail_text);
277             } else {
278             $detail_text = $self->response->decoded_content;
279             chomp($detail_text);
280             }
281             $detail_text =~ s{\r?\n}{\n }xg;
282             return "Details:\n " . $detail_text . "\n";
283             }
284             };
285              
286             =head2 OAuthomatic::Error::Generic
287              
288             Object representing non-HTTP related exception (mostly various cases of bad parameters
289             and programming errors).
290              
291             try {
292             OAuthomatic::Error::Generic->throw({
293             ident => 'Required parameter missing',
294             extra => "Neither body, nor body_params provided."
295             });
296             } catch {
297             my $error = $_;
298             if ($error->isa('OAuthomatic::Error::Generic')) {
299             print "$error\n"; # message
300             print $error->stack_trace->as_string; # if necessary
301             }
302             };
303              
304             =head3 ATTRIBUTES
305              
306             =over 4
307              
308             =item C<ident>
309              
310             Short error description
311              
312             =item C<extra>
313              
314             Additional, more elaborate, information.
315              
316             =back
317              
318             =cut
319              
320             {
321             package OAuthomatic::Error::Generic;
322             use Moose;
323             extends 'OAuthomatic::Error';
324             with 'Role::HasMessage::Errf' => {
325             default => "OAuthomatic internal error: %{ident}s.\n"
326             . "%{extra}s\n",
327             };
328              
329             sub Payload { 'Role::HasPayload::Meta::Attribute::Payload' }
330              
331             has extra => (is => 'ro', isa => 'Str', traits => [Payload]);
332             };
333              
334             1;