File Coverage

lib/OAuthomatic/Error.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


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