File Coverage

blib/lib/Twitter/API/Error.pm
Criterion Covered Total %
statement 29 34 85.2
branch 1 2 50.0
condition 3 9 33.3
subroutine 11 14 78.5
pod 4 6 66.6
total 48 65 73.8


line stmt bran cond sub pod time code
1             package Twitter::API::Error;
2             # ABSTRACT: Twitter API exception
3             $Twitter::API::Error::VERSION = '1.0006';
4 14     14   620 use Moo;
  14         25  
  14         94  
5 14     14   4845 use Ref::Util qw/is_arrayref is_hashref/;
  14         1503  
  14         898  
6 14     14   87 use Try::Tiny;
  14         25  
  14         725  
7 14     14   101 use namespace::clean;
  14         28  
  14         118  
8              
9 14     14   4760 use overload '""' => sub { shift->error };
  14     11   26  
  14         298  
  11         32669  
10              
11             with qw/Throwable StackTrace::Auto/;
12              
13             #pod =method http_request
14             #pod
15             #pod Returns the L<HTTP::Request> object used to make the Twitter API call.
16             #pod
17             #pod =method http_response
18             #pod
19             #pod Returns the L<HTTP::Response> object for the API call.
20             #pod
21             #pod =method twitter_error
22             #pod
23             #pod Returns the inflated JSON error response from Twitter (if any).
24             #pod
25             #pod =cut
26              
27             has context => (
28             is => 'ro',
29             required => 1,
30             handles => {
31             http_request => 'http_request',
32             http_response => 'http_response',
33             twitter_error => 'result',
34             },
35             );
36              
37             #pod =method stack_trace
38             #pod
39             #pod Returns a L<Devel::StackTrace> object encapsulating the call stack so you can discover, where, in your application the error occurred.
40             #pod
41             #pod =method stack_frame
42             #pod
43             #pod Delegates to C<< stack_trace->frame >>. See L<Devel::StackTrace> for details.
44             #pod
45             #pod =method next_stack_fram
46             #pod
47             #pod Delegates to C<< stack_trace->next_frame >>. See L<Devel::StackTrace> for details.
48             #pod
49             #pod =cut
50              
51             has '+stack_trace' => (
52             handles => {
53             stack_frame => 'frame',
54             next_stack_frame => 'next_frame',
55             },
56             );
57              
58             #pod =method error
59             #pod
60             #pod Returns a reasonable string representation of the exception. If Twitter
61             #pod returned error information in the form of a JSON body, it is mined for error
62             #pod text. Otherwise, the HTTP response status line is used. The stack frame is
63             #pod mined for the point in your application where the request initiated and
64             #pod appended to the message.
65             #pod
66             #pod When used in a string context, C<error> is called to stringify exception.
67             #pod
68             #pod =cut
69              
70             has error => (
71             is => 'lazy',
72             );
73              
74             sub _build_error {
75 6     6   52 my $self = shift;
76              
77 6   33     18 my $error = $self->twitter_error_text || $self->http_response->status_line;
78 6         517 my ( $location ) = $self->stack_frame(0)->as_string =~ /( at .*)/;
79 6   50     4206 return $error . ($location || '');
80             }
81              
82             sub twitter_error_text {
83 6     6 0 7 my $self = shift;
84             # Twitter does not return a consistent error structure, so we have to
85             # try each known (or guessed) variant to find a suitable message...
86              
87 6 50       82 return '' unless $self->twitter_error;
88 0         0 my $e = $self->twitter_error;
89              
90             return is_hashref($e) && (
91             # the newest variant: array of errors
92             exists $e->{errors}
93             && is_arrayref($e->{errors})
94             && exists $e->{errors}[0]
95             && is_hashref($e->{errors}[0])
96             && exists $e->{errors}[0]{message}
97             && $e->{errors}[0]{message}
98              
99             # it's single error variant
100             || exists $e->{error}
101             && is_hashref($e->{error})
102             && exists $e->{error}{message}
103             && $e->{error}{message}
104              
105             # the original error structure (still applies to some endpoints)
106             || exists $e->{error} && $e->{error}
107              
108             # or maybe it's not that deep (documentation would be helpful, here,
109             # Twitter!)
110             || exists $e->{message} && $e->{message}
111 0   0     0 ) || ''; # punt
112             }
113              
114             #pod =method twitter_error_code
115             #pod
116             #pod Returns the numeric error code returned by Twitter, or 0 if there is none. See
117             #pod L<https://developer.twitter.com/en/docs/basics/response-codes> for details.
118             #pod
119             #pod =cut
120              
121             sub twitter_error_code {
122 14     14 1 26 my $self = shift;
123              
124 14         220 for ( $self->twitter_error ) {
125             return is_hashref($_)
126             && exists $_->{errors}
127             && exists $_->{errors}[0]
128             && exists $_->{errors}[0]{code}
129             && $_->{errors}[0]{code}
130 14   50     495 || 0;
131             }
132             }
133              
134             #pod =method is_token_error
135             #pod
136             #pod Returns true if the error represents a problem with the access token or its
137             #pod Twitter account, rather than with the resource being accessed.
138             #pod
139             #pod Some Twitter error codes indicate a problem with authentication or the
140             #pod token/secret used to make the API call. For example, the account has been
141             #pod suspended or access to the application revoked by the user. Other error codes
142             #pod indicate a problem with the resource requested. For example, the target account
143             #pod no longer exists.
144             #pod
145             #pod is_token_error returns true for the following Twitter API errors:
146             #pod
147             #pod =for :list
148             #pod * 32: Could not authenticate you
149             #pod * 64: Your account is suspended and is not permitted to access this feature
150             #pod * 88: Rate limit exceeded
151             #pod * 89: Invalid or expired token
152             #pod * 99: Unable to verify your credentials.
153             #pod * 135: Could not authenticate you
154             #pod * 136: You have been blocked from viewing this user's profile.
155             #pod * 215: Bad authentication data
156             #pod * 226: This request looks like it might be automated. To protect our users from
157             #pod spam and other malicious activity, we can’t complete this action right now.
158             #pod * 326: To protect our users from spam…
159             #pod
160             #pod For error 215, Twitter's API documentation says, "Typically sent with 1.1
161             #pod responses with HTTP code 400. The method requires authentication but it was not
162             #pod presented or was wholly invalid." In practice, though, this error seems to be
163             #pod spurious, and often succeeds if retried, even with the same tokens.
164             #pod
165             #pod The Twitter API documentation describes error code 226, but in practice, they
166             #pod use code 326 instead, so we check for both. This error code means the account
167             #pod the tokens belong to has been locked for spam like activity and can't be used
168             #pod by the API until the user takes action to unlock their account.
169             #pod
170             #pod See Twitter's L<Error Codes &
171             #pod Responses|https://dev.twitter.com/overview/api/response-codes> documentation
172             #pod for more information.
173             #pod
174             #pod =cut
175              
176 14     14   7530 use constant TOKEN_ERRORS => (32, 64, 88, 89, 99, 135, 136, 215, 226, 326);
  14         36  
  14         2764  
177             my %token_errors = map +($_ => undef), TOKEN_ERRORS;
178              
179             sub is_token_error {
180 14     14 1 23103 exists $token_errors{shift->twitter_error_code};
181             }
182              
183             #pod =method http_response_code
184             #pod
185             #pod Delegates to C<< http_response->code >>. Returns the HTTP status code of the
186             #pod response.
187             #pod
188             #pod =cut
189              
190 0     0 1   sub http_response_code { shift->http_response->code }
191              
192             #pod =method is_pemanent_error
193             #pod
194             #pod Returns true for HTTP status codes representing an error and with values less
195             #pod than 500. Typically, retrying an API call with one of these statuses right away
196             #pod will simply result in the same error, again.
197             #pod
198             #pod =cut
199              
200 0     0 0   sub is_permanent_error { shift->http_response_code < 500 }
201              
202             #pod =method is_temporary_error
203             #pod
204             #pod Returns true or HTTP status codes of 500 or greater. Often, these errors
205             #pod indicate a transient condition. Retrying the API call right away may result in
206             #pod success. See the L<RetryOnError|Twitter::API::Trait::RetryOnError> for
207             #pod automatically retrying temporary errors.
208             #pod
209             #pod =cut
210              
211 0     0 1   sub is_temporary_error { !shift->is_permanent_error }
212              
213             1;
214              
215             __END__
216              
217             =pod
218              
219             =encoding UTF-8
220              
221             =head1 NAME
222              
223             Twitter::API::Error - Twitter API exception
224              
225             =head1 VERSION
226              
227             version 1.0006
228              
229             =head1 SYNOPSIS
230              
231             use Try::Tiny;
232             use Twitter::API;
233             use Twitter::API::Util 'is_twitter_api_error';
234              
235             my $client = Twitter::API->new(%options);
236              
237             try {
238             my $r = $client->get('account/verify_credentials');
239             }
240             catch {
241             die $_ unless is_twitter_api_error($_);
242              
243             warn "Twitter says: ", $_->twitter_error_text;
244             };
245              
246             =head1 DESCRIPTION
247              
248             Twitter::API dies, throwing a Twitter::API::Error exception when it receives an
249             error. The error object contains information about the error so your code can
250             decide how to respond to various error conditions.
251              
252             =head1 METHODS
253              
254             =head2 http_request
255              
256             Returns the L<HTTP::Request> object used to make the Twitter API call.
257              
258             =head2 http_response
259              
260             Returns the L<HTTP::Response> object for the API call.
261              
262             =head2 twitter_error
263              
264             Returns the inflated JSON error response from Twitter (if any).
265              
266             =head2 stack_trace
267              
268             Returns a L<Devel::StackTrace> object encapsulating the call stack so you can discover, where, in your application the error occurred.
269              
270             =head2 stack_frame
271              
272             Delegates to C<< stack_trace->frame >>. See L<Devel::StackTrace> for details.
273              
274             =head2 next_stack_fram
275              
276             Delegates to C<< stack_trace->next_frame >>. See L<Devel::StackTrace> for details.
277              
278             =head2 error
279              
280             Returns a reasonable string representation of the exception. If Twitter
281             returned error information in the form of a JSON body, it is mined for error
282             text. Otherwise, the HTTP response status line is used. The stack frame is
283             mined for the point in your application where the request initiated and
284             appended to the message.
285              
286             When used in a string context, C<error> is called to stringify exception.
287              
288             =head2 twitter_error_code
289              
290             Returns the numeric error code returned by Twitter, or 0 if there is none. See
291             L<https://developer.twitter.com/en/docs/basics/response-codes> for details.
292              
293             =head2 is_token_error
294              
295             Returns true if the error represents a problem with the access token or its
296             Twitter account, rather than with the resource being accessed.
297              
298             Some Twitter error codes indicate a problem with authentication or the
299             token/secret used to make the API call. For example, the account has been
300             suspended or access to the application revoked by the user. Other error codes
301             indicate a problem with the resource requested. For example, the target account
302             no longer exists.
303              
304             is_token_error returns true for the following Twitter API errors:
305              
306             =over 4
307              
308             =item *
309              
310             32: Could not authenticate you
311              
312             =item *
313              
314             64: Your account is suspended and is not permitted to access this feature
315              
316             =item *
317              
318             88: Rate limit exceeded
319              
320             =item *
321              
322             89: Invalid or expired token
323              
324             =item *
325              
326             99: Unable to verify your credentials.
327              
328             =item *
329              
330             135: Could not authenticate you
331              
332             =item *
333              
334             136: You have been blocked from viewing this user's profile.
335              
336             =item *
337              
338             215: Bad authentication data
339              
340             =item *
341              
342             226: This request looks like it might be automated. To protect our users from spam and other malicious activity, we can’t complete this action right now.
343              
344             =item *
345              
346             326: To protect our users from spam…
347              
348             =back
349              
350             For error 215, Twitter's API documentation says, "Typically sent with 1.1
351             responses with HTTP code 400. The method requires authentication but it was not
352             presented or was wholly invalid." In practice, though, this error seems to be
353             spurious, and often succeeds if retried, even with the same tokens.
354              
355             The Twitter API documentation describes error code 226, but in practice, they
356             use code 326 instead, so we check for both. This error code means the account
357             the tokens belong to has been locked for spam like activity and can't be used
358             by the API until the user takes action to unlock their account.
359              
360             See Twitter's L<Error Codes &
361             Responses|https://dev.twitter.com/overview/api/response-codes> documentation
362             for more information.
363              
364             =head2 http_response_code
365              
366             Delegates to C<< http_response->code >>. Returns the HTTP status code of the
367             response.
368              
369             =head2 is_pemanent_error
370              
371             Returns true for HTTP status codes representing an error and with values less
372             than 500. Typically, retrying an API call with one of these statuses right away
373             will simply result in the same error, again.
374              
375             =head2 is_temporary_error
376              
377             Returns true or HTTP status codes of 500 or greater. Often, these errors
378             indicate a transient condition. Retrying the API call right away may result in
379             success. See the L<RetryOnError|Twitter::API::Trait::RetryOnError> for
380             automatically retrying temporary errors.
381              
382             =head1 AUTHOR
383              
384             Marc Mims <marc@questright.com>
385              
386             =head1 COPYRIGHT AND LICENSE
387              
388             This software is copyright (c) 2015-2021 by Marc Mims.
389              
390             This is free software; you can redistribute it and/or modify it under
391             the same terms as the Perl 5 programming language system itself.
392              
393             =cut