File Coverage

blib/lib/Mojo/UserAgent/Cached.pm
Criterion Covered Total %
statement 260 292 89.0
branch 72 96 75.0
condition 29 73 39.7
subroutine 48 49 97.9
pod 8 12 66.6
total 417 522 79.8


line stmt bran cond sub pod time code
1             package Mojo::UserAgent::Cached;
2              
3 5     5   2531589 use warnings;
  5         82  
  5         191  
4 5     5   32 use strict;
  5         11  
  5         101  
5 5     5   89 use v5.10;
  5         19  
6 5     5   2478 use Algorithm::LCSS;
  5         32891  
  5         268  
7 5     5   3467 use CHI;
  5         404041  
  5         252  
8 5     5   62 use Cwd ();
  5         13  
  5         96  
9 5     5   2758 use Devel::StackTrace;
  5         17792  
  5         249  
10 5     5   2712 use English qw(-no_match_vars);
  5         9115  
  5         32  
11 5     5   1890 use File::Basename;
  5         24  
  5         424  
12 5     5   71 use File::Path;
  5         11  
  5         242  
13 5     5   51 use File::Spec;
  5         40  
  5         179  
14 5     5   38 use List::Util;
  5         10  
  5         307  
15 5     5   38 use Mojo::JSON qw/to_json/;
  5         11  
  5         287  
16 5     5   2110 use Mojo::Transaction::HTTP;
  5         80604  
  5         47  
17 5     5   192 use Mojo::URL;
  5         12  
  5         36  
18 5     5   2610 use Mojo::Log;
  5         69966  
  5         51  
19 5     5   218 use Mojo::Base 'Mojo::UserAgent';
  5         18  
  5         41  
20 5     5   122517 use Mojo::File;
  5         15  
  5         216  
21 5     5   39 use POSIX qw/O_WRONLY O_APPEND O_CREAT/;
  5         14  
  5         65  
22 5     5   3603 use Readonly;
  5         22075  
  5         295  
23 5     5   2823 use String::Truncate;
  5         24526  
  5         41  
24 5     5   1121 use Time::HiRes qw/time/;
  5         17  
  5         128  
25              
26             Readonly my $HTTP_OK => 200;
27             Readonly my $HTTP_FILE_NOT_FOUND => 404;
28              
29             our $VERSION = '1.25';
30              
31             # TODO: Timeout, fallback
32             # TODO: Expected result content (json etc)
33              
34             # MOJO_USERAGENT_CONFIG
35             ## no critic (ProhibitMagicNumbers)
36             has 'connect_timeout' => sub { $ENV{MOJO_CONNECT_TIMEOUT} // 10 };
37             has 'inactivity_timeout' => sub { $ENV{MOJO_INACTIVITY_TIMEOUT} // 20 };
38             has 'max_redirects' => sub { $ENV{MOJO_MAX_REDIRECTS} // 4 };
39             has 'request_timeout' => sub { $ENV{MOJO_REQUEST_TIMEOUT} // 0 };
40             ## use critic
41              
42             # MUAC_CLIENT_CONFIG
43             has 'local_dir' => sub { $ENV{MUAC_LOCAL_DIR} // q{} };
44             has 'always_return_file' => sub { $ENV{MUAC_ALWAYS_RETURN_FILE} // undef };
45              
46             has 'cache_agent' => sub {
47             $ENV{MUAC_NOCACHE} ? () : CHI->new(
48             driver => $ENV{MUAC_CACHE_DRIVER} || 'File',
49             root_dir => $ENV{MUAC_CACHE_ROOT_DIR} || '/tmp/mojo-useragent-cached',
50             serializer => $ENV{MUAC_CACHE_SERIALIZER} || 'Storable',
51             namespace => $ENV{MUAC_CACHE_NAMESPACE} || 'MUAC_Client',
52             expires_in => $ENV{MUAC_CACHE_EXPIRES_IN} // '1 minute',
53             expires_on_backend => $ENV{MUAC_CACHE_EXPIRES_ON_BACKEND} // 1,
54             max_key_length => 140,
55             %{ shift->cache_opts || {} },
56             )
57             };
58             has 'cache_opts' => sub { {} };
59             has 'cache_url_opts' => sub { {} };
60             has 'key_generator' => sub { \&key_generator_cb; };
61             has 'logger' => sub { Mojo::Log->new() };
62             has 'access_log' => sub { $ENV{MUAC_ACCESS_LOG} || '' };
63             has 'use_expired_cached_content' => sub { $ENV{MUAC_USE_EXPIRED_CACHED_CONTENT} // 1 };
64             has 'accepted_error_codes' => sub { $ENV{MUAC_ACCEPTED_ERROR_CODES} || '' };
65             has 'sorted_queries' => 1;
66              
67             has 'created_stacktrace' => '';
68              
69             sub new {
70 37     37 1 192346 my ($class, %opts) = @_;
71              
72 37         143 my %mojo_agent_config = map { $_ => $opts{$_} } grep { exists $opts{$_} } qw/
  3         16  
  592         1065  
73             ca
74             cert
75             connect_timeout
76             cookie_jar
77             inactivity_timeout
78             insecure
79             ioloop
80             key
81             local_address
82             max_connections
83             max_redirects
84             max_response_size
85             proxy
86             request_timeout
87             server
88             transactor
89             /;
90              
91 37         211 my $ua = $class->SUPER::new(%mojo_agent_config);
92              
93             # Populate attributes
94 37         282 map { $ua->$_( $opts{$_} ) } grep { exists $opts{$_} } qw/
  5         53  
  370         656  
95             local_dir
96             always_return_file
97             cache_opts
98             cache_agent
99             cache_url_opts
100             logger
101             access_log
102             use_expired_cached_content
103             accepted_error_codes
104             sorted_queries
105             /;
106              
107 37         157 $ua->created_stacktrace($ua->_get_stacktrace);
108              
109 37         855 return bless($ua, $class);
110             }
111              
112              
113             sub invalidate {
114 6     6 1 1819 my ($self, $key) = @_;
115              
116 6 50       22 if ($self->is_cacheable($key)) {
117 6         42 $self->logger->debug("Invalidating cache for '$key'");
118 6         1340 return $self->cache_agent->remove($key);
119             }
120              
121 0         0 return;
122             }
123              
124             sub expire {
125 2     2 1 8 my ($self, $key) = @_;
126              
127 2 50       14 if ($self->is_cacheable($key)) {
128 2         20 $self->logger->debug("Expiring cache for '$key'");
129 2         418 return $self->cache_agent->expire($key);
130             }
131              
132 0         0 return;
133             }
134              
135             sub build_tx {
136 101     101 1 2785803 my ($self, $method, $url, @more) = @_;
137              
138 101   66     392 $url = ($self->always_return_file || $url);
139              
140 101 100       1098 if ($url !~ m{^(/|[^/]+:)}) {
141 11 100       31 if ($self->local_dir) {
    50          
    50          
142 5         32 $url = 'file://' . File::Spec->catfile($self->local_dir, "$url");
143             } elsif ($self->always_return_file) {
144 0         0 $url = 'file://' . "$url";
145             } elsif ($url !~ m{^(/|[^/]+:)}) {
146 6         316 $url = 'file://' . Cwd::realpath("$url");
147             }
148             }
149              
150 101         785 $self->transactor->tx($method, $url, @more);
151             }
152              
153             sub start {
154 100     100 1 27002 my ($self, $tx, $cb) = @_;
155              
156 100         332 my $url = $tx->req->url->to_unsafe_string;
157 100         18757 my $method = $tx->req->method;
158 100         798 my $headers = $tx->req->headers->to_hash(1);
159 100         4206 my $content = $tx->req->content->asset->slurp;
160 100         2279 delete $headers->{'User-Agent'};
161 100         213 delete $headers->{'Accept-Encoding'};
162 100 100 100     301 my @opts = (($method eq 'GET' ? () : $method), (keys %{ $headers || {} } ? $headers : ()), $content || ());
  100 50       695  
    100          
163 100         358 my $key = $self->generate_key($url, @opts);
164 100         332 my $start_time = time;
165              
166             # Fork-safety
167 100 100 100     643 $self->_cleanup->server->restart if $self->{pid} && $self->{pid} ne $$;
168 100   66     3890 $self->{pid} //= $$;
169              
170             # We wrap the incoming callback in our own callback to be able to cache the response
171             my $wrapper_cb = $cb ? sub {
172 22     22   153577 my ($ua, $tx) = @_;
173 22         96 $cb->($ua, $ua->_post_process_get($tx, $start_time, $key, @opts));
174 100 100       377 } : ();
175              
176             # Is an absolute URL or an URL relative to the app eg. http://foo.com/ or /foo.txt
177 100 100 66     545 if ($url !~ m{ \A file:// }gmx && (Mojo::URL->new($url)->is_abs || ($url =~ m{ \A / }gmx && !$self->always_return_file))) {
      66        
178 88 100       6749 if ($self->is_cacheable($key)) {
179 27         1481 my $serialized = $self->cache_agent->get($key);
180 27 100       14413 if ($serialized) {
181 14         55 $serialized->{events} = $tx->{events};
182 14         83 $serialized->{req_events} = $tx->req->{events};
183 14         133 $serialized->{res_events} = $tx->res->{events};
184 14         272 my $cached_tx = _build_fake_tx($serialized);
185 14         136 $self->_log_line($cached_tx, {
186             start_time => $start_time,
187             key => $key,
188             type => 'cached result',
189             });
190 14         142 $cached_tx->req->finish;
191 14         345 $cached_tx->res->finish;
192 14         303 $cached_tx->closed;
193 14 100       265 return $cb->($self, $cached_tx) if $cb;
194 12         278 return $cached_tx;
195             }
196             }
197              
198             # Non-blocking
199 74 100       135836 if ($wrapper_cb) {
200 22         41 warn "-- Non-blocking request (@{[_url($tx)]})\n" if Mojo::UserAgent::DEBUG;
201 22         115 return $self->_start(Mojo::IOLoop->singleton, $tx, $wrapper_cb);
202             }
203              
204             # Blocking
205 52         110 warn "-- Blocking request (@{[_url($tx)]})\n" if Mojo::UserAgent::DEBUG;
206 52     52   229 $self->_start($self->ioloop, $tx => sub { shift->ioloop->stop; $tx = shift });
  52         1236309  
  52         1045  
207 52         67291 $self->ioloop->start;
208              
209 52         8156 return $self->_post_process_get( $tx, $start_time, $key, @opts );
210             } else { # Local file eg. t/data/foo.txt or file://.*/
211 12         69 $url =~ s{file://}{};
212 12         77 my $code = $HTTP_FILE_NOT_FOUND;
213 12         81 my $res;
214 12 100       24 eval {
215 12         33 $res = $self->_parse_local_file_res($url);
216 10         54 $code = $res->{code};
217             } or $self->logger->error($EVAL_ERROR);
218              
219 12         643 my $params = { url => $url, body => $res->{body}, code => $code, method => 'FILE', headers => $res->{headers}, events => $tx->{events}, req_events => $tx->req->{events}, res_events => $tx->res->{events} };
220              
221             # first non-blocking, if no callback, regular post process
222 12         297 my $tx = _build_fake_tx($params);
223 12         78 $self->_log_line($tx, {
224             start_time => $start_time,
225             key => $key,
226             type => 'local file',
227             });
228              
229 12 100       80 return $cb->($self, $tx) if $cb;
230 11         109 return $tx;
231             }
232              
233 0         0 return $tx;
234             }
235              
236             sub _post_process_get {
237 74     74   277 my ($self, $tx, $start_time, $key) = @_;
238              
239 74 100 66     230 if ( $tx->req->url->scheme ne 'file' && $self->is_cacheable($key) ) {
240 13 100       250 if ( $self->is_considered_error($tx) ) {
241             # Return an expired+cached version of the page for other errors
242 4 50       67 if ( $self->use_expired_cached_content ) { # TODO: URL by URL, and case-by-case expiration
243 4 100       21 if (my $cache_obj = $self->cache_agent->get_object($key)) {
244 1         284 my $serialized = $cache_obj->value;
245 1         154 $serialized->{headers}->{'X-Mojo-UserAgent-Cached-ExpiresAt'} = $cache_obj->expires_at($key);
246 1         6 $serialized->{events} = $tx->{events};
247 1         4 $serialized->{req_events} = $tx->req->{events};
248 1         10 $serialized->{res_events} = $tx->res->{events};
249              
250 1         6 my $expired_tx = _build_fake_tx($serialized);
251 1         10 $self->_log_line( $expired_tx, {
252             start_time => $start_time,
253             key => $key,
254             type => 'expired and cached',
255             orig_tx => $tx,
256             });
257 1         14 $expired_tx->req->finish;
258 1         22 $expired_tx->res->finish;
259 1         18 $expired_tx->closed;
260              
261 1         101 return $expired_tx;
262             }
263             }
264             } else {
265             # Store object in cache
266 9         174 $self->cache_agent->set($key, _serialize_tx($tx), $self->_cache_url_opts($tx->req->url));
267             }
268             }
269              
270 73         15282 $self->_log_line($tx, {
271             start_time => $start_time,
272             key => $key,
273             type => 'fetched',
274             });
275              
276 73         1759 return $tx;
277             }
278              
279             sub _cache_url_opts {
280 9     9   3825 my ($self, $url) = @_;
281 9 50   1   54 my ($pat, $opts) = List::Util::pairfirst { $url =~ /$a/; } %{ $self->cache_url_opts || {} };
  1         34  
  9         38  
282 9   66     368 return $opts || ();
283             }
284              
285             sub set {
286 1     1 1 10 my ($self, $url, $value, @opts) = @_;
287              
288 1         5 my $key = $self->generate_key($url, @opts);
289 1 50 0     6 $self->logger->debug("Illegal cache key: $key") && return if ref $key;
290              
291 1         22 my $fake_tx = _build_fake_tx({
292             url => $key,
293             body => $value,
294             code => $HTTP_OK,
295             method => 'FILE'
296             });
297              
298 1         6 $self->logger->debug("Set cache key: $key");
299 1         231 $self->cache_agent->set($key, _serialize_tx($fake_tx));
300 1         1804 return $key;
301             }
302              
303             sub is_valid {
304 4     4 0 2336 my ($self, $key) = @_;
305              
306 4 100 50     20 ($self->logger->debug("Illegal cache key: $key") && return) if ref $key;
307              
308 3         11 $self->logger->debug("Checking if key is valid: $key");
309 3         536 return $self->cache_agent->is_valid($key);
310             }
311              
312             sub is_cacheable {
313 133     133 0 1051 my ($self, $url) = @_;
314              
315 133   66     383 return $self->cache_agent && ($url !~ m{ \A / }gmx);
316             }
317              
318             sub generate_key {
319 113     113 1 7132 my ($self, $url, @opts) = @_;
320              
321 113         375 return $self->key_generator->($self, $url, @opts);
322             }
323              
324             sub key_generator_cb {
325 113     113 0 1089 my ($self, $url, @opts) = @_;
326              
327 113 100       314 my $key = join q{,}, $self->sort_query($url), (@opts ? to_json(@opts > 1 ? \@opts : $opts[0]) : ());
    100          
328              
329 113         1726 return $key;
330             }
331              
332             sub is_considered_error {
333 13     13 0 36 my ($self, $tx) = @_;
334              
335             # If we find some error codes that should be accepted, we don't consider this an error
336 13 100 100     45 if ( $tx->error && $self->accepted_error_codes ) {
337 2 50       66 my $codes = ref $self->accepted_error_codes ? $self->accepted_error_codes
338             : [ ( $self->accepted_error_codes ) ];
339 2 50   2   28 return if List::Util::first { $tx->error->{code} == $_ } @{$codes};
  2         8  
  2         11  
340             }
341              
342 11         201 return $tx->error;
343             }
344              
345             sub sort_query {
346 119     119 1 3652 my ($self, $url) = @_;
347 119 50       331 return $url unless $self->sorted_queries;
348              
349 119 100       1035 $url = Mojo::URL->new($url) unless ref $url eq 'Mojo::URL';
350              
351 119 100       9207 my $flattened_sorted_url = ($url->protocol ? ( $url->protocol . '://' ) : '' ) .
    100          
    100          
    50          
352             ($url->userinfo ? ( $url->userinfo . '@' ) : '' ) .
353             ($url->host ? ( $url->host_port ) : '' ) .
354             ($url->path ? ( $url->path ) : '' ) ;
355              
356 7 100   21   34 $flattened_sorted_url .= '?' . join '&', sort { $a cmp $b } List::Util::pairmap { (($b ne '') ? (join '=', $a, $b) : $a); } @{ $url->query }
  21         432  
  14         1682  
357 119 100       10886 if scalar @{ $url->query };
  119         357  
358              
359 119         3420 return $flattened_sorted_url;
360             }
361              
362             sub _serialize_tx {
363 10     10   1657 my ($tx) = @_;
364              
365 10         31 $tx->res->headers->header('X-Mojo-UserAgent-Cached', time);
366              
367             return {
368 10         533 method => $tx->req->method,
369             url => $tx->req->url,
370             code => $tx->res->code,
371             body => $tx->res->body,
372             json => $tx->res->json,
373             headers => $tx->res->headers->to_hash,
374             };
375             }
376              
377             sub _build_fake_tx {
378 28     28   83 my ($opts) = @_;
379              
380             # Create transaction object to return so we look like a regular request
381 28         124 my $tx = Mojo::Transaction::HTTP->new();
382              
383 28         342 $tx->req->method($opts->{method});
384 28         1795 $tx->req->url(Mojo::URL->new($opts->{url}));
385              
386 28         5544 $tx->res->headers->from_hash($opts->{headers});
387              
388 28         3057 my $now = time;
389 28   66     72 $tx->res->headers->header('X-Mojo-UserAgent-Cached-Age', $now - ($tx->res->headers->header('X-Mojo-UserAgent-Cached') || $now));
390              
391 28         1780 $tx->res->code($opts->{code});
392 28         307 $tx->res->{json} = $opts->{json};
393 28         145 $tx->res->body($opts->{body});
394              
395 28         1361 $tx->{events} = $opts->{events};
396 28         75 $tx->req->{events} = $opts->{req_events};
397 28         143 $tx->res->{events} = $opts->{res_events};
398              
399 28         122 return $tx;
400             }
401              
402             sub _parse_local_file_res {
403 12     12   31 my ($self, $url) = @_;
404              
405 12         20 my $headers;
406 12         78 my $body = Mojo::File->new($url)->slurp;
407 10         1727 my $code = $HTTP_OK;
408 10         60 my $msg = 'OK';
409              
410 10 100       62 if ($body =~ m{\A (?: DELETE | GET | HEAD | OPTIONS | PATCH | POST | PUT ) \s }gmx) {
411 2         13 my $code_msg_headers;
412             my $code_msg;
413 2         0 my $http;
414 2         0 my $msg;
415 2         40 (undef, $code_msg_headers, $body) = split m{(?:\r\n|\n){2,}}mx, $body, 3; ## no critic (ProhibitMagicNumbers)
416 2         36 ($code_msg, $headers) = split m{(?:\r\n|\n)}mx, $code_msg_headers, 2;
417 2         17 ($http, $code, $msg) = $code_msg =~ m{ \A (?:(\S+) \s+)? (\d+) \s+ (.*) \z}mx;
418              
419 2         18 $headers = Mojo::Headers->new->parse("$headers\n\n")->to_hash;
420             }
421              
422 10         524 return { body => $body, code => $code, message => $msg, headers => $headers };
423             }
424              
425             sub _write_local_file_res {
426 100     100   389 my ($self, $tx, $dir) = @_;
427              
428 100 0 33     358 return unless ($dir && -e $dir && -d $dir);
      33        
429              
430 0         0 my $method = $tx->req->method;
431 0         0 my $url = $tx->req->url;
432 0         0 my $body = $tx->res->body;
433 0         0 my $code = $tx->res->code;
434 0         0 my $message = $tx->res->message;
435              
436 0         0 my $target_file = File::Spec->catfile($dir, split '/', $url->path_query);
437 0         0 File::Path::make_path(File::Basename::dirname($target_file));
438 0 0       0 Mojo::File->new($target_file)->spurt((
439             join "\n\n",
440             (join " ", $method, "$url\n" ) . $tx->req->headers->to_string,
441             (join " ", $code, "$message\n") . $tx->res->headers->to_string,
442             $body
443             )
444             ) and $self->logger->debug("Wrote request+response to: '$target_file'");
445             }
446              
447             sub _log_line {
448 100     100   257 my ($self, $tx, $opts) = @_;
449              
450 100         515 $self->_write_local_file_res($tx, $ENV{MUAC_CLIENT_WRITE_LOCAL_FILE_RES_DIR});
451              
452 100         331 my $callers = $self->_get_stacktrace;
453 100         1410 my $created_stacktrace = $self->created_stacktrace;
454              
455             # Remove common parts to get smaller created stacktrace
456 100         1044 my $strings = Algorithm::LCSS::CSS_Sorted( [ split /,/, $callers ] , [ split /,/, $created_stacktrace ] );
457             map {
458 24         40 my @lcss = @{$_};
  24         73  
459 24         93 my $pat = join ",", @lcss[1..$#lcss-1];
460 24 50       89 if (scalar @lcss > 2) { $created_stacktrace =~ s{$pat}{,}mx }
  24         255  
461 100 50       14423 } @{ $strings || [] };
  100         330  
462              
463             $self->logger->debug(sprintf(q{Returning %s '%s' => %s for %s (%s)}, (
464             $opts->{type},
465             String::Truncate::elide( $tx->req->url, 150, { truncate => 'middle'} ),
466 100   33     361 ($tx->res->code || $tx->res->error->{code} || $tx->res->error->{message}),
467             $callers, $created_stacktrace
468             )));
469              
470 100 50       62195 return unless $self->access_log;
471              
472 0         0 my $elapsed_time = sprintf '%.3f', (time-$opts->{start_time});
473              
474 0         0 my $NONE = q{-};
475              
476 0   0     0 my $http_host = $tx->req->url->host || $NONE;
477 0         0 my $remote_addr = $NONE;
478 0   0     0 my $time_local = POSIX::strftime('%d/%b/%Y:%H:%M:%S %z', localtime) || $NONE;
479 0   0     0 my $request = ($tx->req->method . q{ } . $tx->req->url->path_query) || $NONE;
480 0   0     0 my $status = $tx->res->code || $NONE;
481 0   0     0 my $body_bytes_sent = length $tx->res->body || $NONE;
482 0   0     0 my $http_referer = $callers || $NONE;
483 0   0     0 my $http_user_agent = __PACKAGE__ . "(" . $opts->{type} .")" || $NONE;
484 0   0     0 my $request_time = $elapsed_time || $NONE;
485 0   0     0 my $upstream_response_time = $elapsed_time || $NONE;
486 0         0 my $http_x_forwarded_for = $NONE;
487              
488             # Use sysopen, slightly slower and hits disk, but avoids clobbering
489 0         0 sysopen my $fh, $self->access_log, O_WRONLY | O_APPEND | O_CREAT; ## no critic (ProhibitBitwiseOperators)
490 0 0       0 syswrite $fh, qq{$http_host $remote_addr [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" $request_time $upstream_response_time "$http_x_forwarded_for"\n}
491             or $self->logger->warn("Unable to write to '" . $self->access_log . "': $OS_ERROR");
492 0 0       0 close $fh or $self->logger->warn("Unable to close '" . $self->access_log . "': $OS_ERROR");
493              
494 0         0 return;
495             }
496              
497             sub _get_stacktrace {
498 137     137   315 my ($self) = @_;
499              
500             my @frames = ( Devel::StackTrace->new(
501             ignore_class => [ 'Devel::StackTrace', 'Mojo::UserAgent::Cached', 'Template::Document', 'Template::Context', 'Template::Service' ],
502 618     618   107222 frame_filter => sub { ($_[0]->{caller}->[0] !~ m{ \A Mojo | Try }gmx) },
503 137         1299 )->frames() );
504              
505 137         10142 my $prev_package = '';
506             my $callers = join q{,}, map {
507 299         1508 my $package = $_->package;
508 299 50       1865 if ($package eq 'Template::Provider') {
509 0         0 $package = (join "/", grep { $_ } (split '/', $_->filename)[-3..-1]);
  0         0  
510             }
511 299 100       688 if ($prev_package eq $package) {
512 42         84 $package = '';
513             } else {
514 257         488 $prev_package = $package;
515 257         981 $package =~ s/(?:(\w)\w*::)/$1./gmx;
516 257         552 $package .= ':';
517             }
518 299         686 $package . $_->line();
519 137         345 } grep { $_ } @frames;
  299         588  
520             }
521              
522 0     0     sub _url { shift->req->url->to_abs }
523              
524             1;
525              
526             =encoding utf8
527              
528             =head1 NAME
529              
530             Mojo::UserAgent::Cached - Caching, Non-blocking I/O HTTP, Local file and WebSocket user agent
531              
532             =head1 SYNOPSIS
533              
534             use Mojo::UserAgent::Cached;
535              
536             my $ua = Mojo::UserAgent::Cached->new;
537              
538             =head1 DESCRIPTION
539              
540             L is a full featured caching, non-blocking I/O HTTP, Local file and WebSocket user
541             agent, with IPv6, TLS, SNI, IDNA, Comet (long polling), keep-alive, connection
542             pooling, timeout, cookie, multipart, proxy, gzip compression and multiple
543             event loop support.
544              
545             It inherits all of the features L provides but in addition allows you to
546             retrieve cached content using a L compatible caching engine.
547              
548             See L and L for more.
549              
550             =head1 ATTRIBUTES
551              
552             L inherits all attributes from L and implements the following new ones.
553              
554             =head2 local_dir
555              
556             my $local_dir = $ua->local_dir;
557             $ua->local_dir('/path/to/local_files');
558              
559             Sets the local dir, used as a prefix where relative URLs are fetched from. A C request would
560             read the file '/tmp/foobar.txt' if local_dir is set to '/tmp', defaults to the value of the
561             C environment variable and if not set, to ''.
562              
563             =head2 always_return_file
564              
565             my $file = $ua->always_return_file;
566             $ua->always_return_file('/tmp/default_file.txt');
567              
568             Makes all consecutive request return the same file, no matter what file or URL is requested with C, defaults
569             to the value of the C environment value and if not, it respects the File/URL in the request.
570              
571             =head2 cache_agent
572              
573             my $cache_agent = $ua->cache_agent;
574             $ua->cache_agent(CHI->new(
575             driver => $ENV{MUAC_CACHE_DRIVER} || 'File',
576             root_dir => $ENV{MUAC_CACHE_ROOT_DIR} || '/tmp/mojo-useragent-cached',
577             serializer => $ENV{MUAC_CACHE_SERIALIZER} || 'Storable',
578             namespace => $ENV{MUAC_CACHE_NAMESPACE} || 'MUAC_Client',
579             expires_in => $ENV{MUAC_CACHE_EXPIRES_IN} // '1 minute',
580             expires_on_backend => $ENV{MUAC_CACHE_EXPIRES_ON_BACKEND} // 1,
581             ));
582              
583             Tells L which cache_agent to use. It needs to be CHI-compliant and defaults to the above settings.
584              
585             You may also set the C<$ENV{MUAC_NOCACHE}> environment variable to avoid caching at all.
586              
587             =head2 cache_opts
588              
589             my $cache_opts = $ua->cache_opts;
590             $ua->cache_opts({ expires_in => '5 minutes' });
591              
592             Allows passing in cache options that will be appended to existing options in default cache agent creation.
593              
594             =head2 cache_url_opts
595              
596             my $urls_href = $ua->cache_url_opts;
597             $ua->cache_url_opts({
598             'https?://foo.com/long-lasting-data.*' => { expires_in => '2 weeks' }, # Cache some data two weeks
599             '.*' => { expires_at => 0 }, # Don't store anything in cache
600             });
601            
602             Accepts a hash ref of regexp strings and expire times, this allows you to define cache validity time for individual URLs, hosts etc.
603             The first match will be used.
604              
605             =head2 key_generator
606              
607             A callback method to generate keys. The method gets ($self, $url, @opts) passed as parameters. The default is set to C
608              
609             =head2 logger
610              
611             Provide a logging object, defaults to Mojo::Log
612              
613             # Example:
614             # Returning fetched 'https://graph.facebook.com?ids=http%3A%2F%2Fexample.com%2Flivet%2F20...-lommebok&access_token=1234' => 200 for A.C.Facebook:133,185,183,A.M.F.ArticleList:19,9,A.M.Selector:47,responsive/modules/most-shared.html.tt:15,15,13,templates/inc/macros.tt:125,138,templates/responsive/frontpage.html.tt:10,10,16,Template:66,A.G.C.Article:338,147,main:14 (A.C.Facebook:68,E.C.Sandbox_874:7,A.C.Facebook:133,,,main:14)
615              
616             Format:
617             Returning '' => 'HTTP code' for ()
618              
619             cache-status: (cached|fetched|cached+expired)
620             URL: the URL requested, shortened when it is really long
621             request_stacktrace: Simplified stacktrace with leading module names shortened, also includes TT stacktrace support. Line numbers in the same module are grouped (order kept of course).
622             created_stacktrace: Stack trace for creation of UA object, useful to see what options went in, and which object is used. Same format as normal stacktrace, but skips common parts.
623            
624             Example:
625             created_stacktrace: A.C.Facebook:68,E.C.Sandbox_874:7,A.C.Facebook:133,,main:14
626             stacktrace: A.C.Facebook:133,< common part: 185,183,A.M.F.ArticleList:19,9,A.M.Selector:47,responsive/modules/most-shared.html.tt:15,15,13,templates/inc/macros.tt:125,138,templates/responsive/frontpage.html.tt:10,10,16,Template:66,A.G.C.Article:338,147 >,main:14
627              
628             =head2 access_log
629              
630             A file that will get logs of every request, the format is a hybrid of Apache combined log, including time spent for the request.
631             If provided the file will be written to. Defaults to C<$ENV{MUAC_ACCESS_LOG} || ''> which means no log will be written.
632              
633             =head2 use_expired_cached_content
634              
635             Indicates that we will send expired, cached content back. This means that if a request fails, and the cache has expired, you
636             will get back the last successful content. Defaults to C<$ENV{MUAC_EXPIRED_CONTENT} // 1>
637              
638             =head2 accepted_error_codes
639              
640             A list of error codes that should not be considered as errors. For instance this means that the client will not look for expired
641             cached content for requests that result in this response. Defaults to C<$ENV{MUAC_ACCEPTED_ERROR_CODES} || ''>
642              
643             =head2 sorted_queries
644              
645             Setting this to a true value will sort query parameters in the resulting URL. This means that requests will be identical if the key/value pairs
646             are the same. This helps when URLs have been built up using hashes that may have random orders.
647              
648             =head1 OVERRIDEN ATTRIBUTES
649              
650             In addition L overrides the following L attributes.
651              
652             =head2 connect_timeout
653              
654             Defaults to C<$ENV{MOJO_CONNECT_TIMEOUT} // 2>
655              
656             =head2 inactivity_timeout
657              
658             Defaults to C<$ENV{MOJO_INACTIVITY_TIMEOUT} // 5>
659              
660             =head2 max_redirects
661              
662             Defaults to C<$ENV{MOJO_MAX_REDIRECTS} // 4>
663              
664             =head2 request_timeout
665              
666             Defaults to C<$ENV{MOJO_REQUEST_TIMEOUT} // 10>
667              
668             =head1 METHODS
669              
670             L inherits all methods from L and
671             implements the following new ones.
672              
673             =head2 invalidate
674              
675             $ua->invalidate($key);
676              
677             Deletes the cache of the given $key.
678              
679             =head2 expire
680              
681             $ua->expire($key);
682              
683             Set the cache of the given $key as expired.
684              
685             =head2 set
686              
687             my $tx = $ua->build_tx(GET => "http://localhost:$port", ...);
688             $tx = $ua->start($tx);
689             my $cache_key = $ua->generate_key("http://localhost:$port", ...);
690             $ua->set($cache_key, $tx);
691              
692             Set allows setting data directly for a given URL
693              
694             =head2 generate_key(@params)
695              
696             Returns a key to be used for the cache agent. It accepts the same parameters
697             that a normal ->get() request does.
698              
699             =head2 validate_key
700              
701             my $status = $ua4->validate_key('http://example.com');
702              
703             Fast validates if key is valid in cache without doing fetch.
704             Return 1 if true.
705              
706             =head2 sort_query($url)
707              
708             Returns a string with the URL passed, with sorted query parameters suitable for cache lookup
709              
710             =head1 OVERRIDEN METHODS
711              
712             =head2 new
713              
714             my $ua = Mojo::UserAgent::Cached->new( request_timeout => 1, ... );
715              
716             Accepts the attributes listed above and all attributes from L.
717             Stores its own attributes and passes on the relevant ones when creating a
718             parent L object that it inherits from. Returns a L object
719              
720             =head2 get(@params)
721              
722             my $tx = $ua->get('http://example.com');
723              
724             Accepts the same arguments and returns the same as L.
725              
726             It will try to return a cached version of the $url, adhering to the set or default attributes.
727              
728             In addition if a relative file path is given, it tries to return the file appended to
729             the attribute C. In this case a fake L object is returned,
730             populated with a L with method and url, and a L
731             with headers, code and body set.
732              
733             =head1 ENVIRONMENT VARIABLES
734              
735             C<$ENV{MUAC_CLIENT_WRITE_LOCAL_FILE_RES_DIR}> can be set to a directory to store a request in:
736              
737             # Re-usable local file with headers and metadata ends up at 't/data/dir/lol/foo.html?bar=1'
738             $ENV{MUAC_CLIENT_WRITE_LOCAL_FILE_RES_DIR}='t/data/dir';
739             Mojo::UserAgent::Cached->new->get("http://foo.com/lol/foo.html?bar=1");
740              
741             =head1 SEE ALSO
742              
743             L, L, L, L.
744              
745             =head1 COPYRIGHT
746              
747             Nicolas Mendoza (2015-), ABC Startsiden (2015)
748              
749             =head1 LICENSE
750              
751             Same as Perl licence as per agreement with ABC Startsiden on 2015-06-02
752              
753             =cut