File Coverage

blib/lib/LWP/UserAgent/Role/CHICaching.pm
Criterion Covered Total %
statement 4 6 66.6
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 6 8 75.0


line stmt bran cond sub pod time code
1             package LWP::UserAgent::Role::CHICaching;
2              
3 1     1   11448 use 5.006000;
  1         4  
  1         44  
4 1     1   278 use CHI;
  0            
  0            
5             use Moo::Role;
6             use Types::Standard qw(Str Bool Ref InstanceOf);
7             use Types::URI -all;
8              
9             our $AUTHORITY = 'cpan:KJETILK';
10             our $VERSION = '0.03';
11              
12             =pod
13              
14             =encoding utf-8
15              
16             =head1 NAME
17              
18             LWP::UserAgent::Role::CHICaching - A role to allow LWP::UserAgent to cache with CHI
19              
20             =head1 SYNOPSIS
21              
22             Compose it into a class, e.g.
23              
24             package LWP::UserAgent::MyCacher;
25             use Moo;
26             extends 'LWP::UserAgent';
27             with 'LWP::UserAgent::Role::CHICaching', 'LWP::UserAgent::Role::CHICaching::SimpleKeyGen';
28              
29              
30             =head1 DESCRIPTION
31              
32             This is a role for creating caching user agents. When the client makes
33             a request to the server, sometimes the response should be cached, so
34             that no actual request has to be sent at all, or possibly just a
35             request to validate the cache. HTTP 1.1 defines how to do this. This
36             role makes it possible to use the very flexible L<CHI> module to
37             manage such a cache. See L<LWP::UserAgent::CHICaching> for a finished
38             class you can use.
39              
40              
41             =head2 Attributes and Methods
42              
43             =over
44              
45             =item C<< cache >>
46              
47             Used to set the C<CHI> object to be used as cache in the constructor.
48              
49              
50              
51             =item C<< request_uri >>
52              
53             The Request-URI of the request. When set, it will clear the C<key>,
54             but should probably be left to be used internally for now.
55              
56             =item C<< request >>
57              
58             Wrapping L<LWP::UserAgent>'s request method.
59              
60             =item C<< is_shared >>
61              
62             A boolean value to set whether the cache can be shared. The default is
63             that it is.
64              
65             =item C<< heuristics_opts >>
66              
67             A hashref that is passed to the C<freshness_lifetime> method of
68             L<HTTP::Response>, and used to determine the behaviour of the
69             heuristic lifetime. By default, heuristic freshness lifetime is off,
70             only standards-compliant freshness lifetime (i.e. based on the
71             C<Cache-Control> and C<Expires> headers) are used.
72              
73             =back
74              
75             =head2 Implemented elsewhere
76              
77             The following are required by this role, but implemented
78             elsewhere. See L<LWP::UserAgent::Role::CHICaching::SimpleKeyGen> for
79             further explanations.
80              
81             =over
82              
83             =item C<< key >>, C<< clear_key >>
84              
85             The key to use for a response.
86              
87             =item C<< cache_vary($response) >>
88              
89             A method that returns true if the response may be cached even if it
90             contains a C<Vary> header, false otherwise. The L<HTTP::Response>
91             object will be passed to it as a parameter.
92              
93              
94             =back
95              
96             =cut
97              
98             has cache => (
99             is => 'ro',
100             isa => InstanceOf['CHI::Driver'],
101             required => 1,
102             );
103              
104              
105             requires 'key';
106             requires 'cache_vary';
107              
108             has request_uri => (
109             is =>'rw',
110             isa => Uri,
111             coerce => 1,
112             trigger => sub { shift->clear_key },
113             );
114              
115             has is_shared => (
116             is => 'rw',
117             isa => Bool,
118             default => 1);
119              
120             has heuristics_opts => (
121             is => 'rw',
122             isa => Ref['HASH'],
123             default => sub {return {heuristic_expiry => 0}}
124             );
125              
126             around request => sub {
127             my ($orig, $self) = (shift, shift);
128             my @args = @_;
129             my $request = $args[0];
130              
131             return $self->$orig(@args) if $request->method ne 'GET';
132              
133             $self->request_uri($request->uri);
134              
135             my $cached = $self->cache->get($self->key); # CHI will take care of expiration
136              
137             my $expires_in = 0;
138             if (defined($cached)) {
139             ######## Here, we decide whether to reuse a cached response.
140             ######## The standard describing this is:
141             ######## http://tools.ietf.org/html/rfc7234#section-4
142             $cached->header('Age' => $cached->current_age);
143             return $cached;
144             } else {
145             my $res = $self->$orig(@args);
146              
147             ######## Here, we decide whether to store a response
148             ######## This is defined in:
149             ######## http://tools.ietf.org/html/rfc7234#section-3
150             # Quoting the standard
151              
152             ## A cache MUST NOT store a response to any request, unless:
153            
154             ## o The request method is understood by the cache and defined as being
155             ## cacheable, and
156             # TODO: Ok, only GET supported, see above
157              
158             ## o the response status code is understood by the cache, and
159             if ($res->is_success) { # TODO: Cache only successful responses for now
160              
161             # First, we deal superficially with the Vary header, for the
162             # full complexity see
163             # http://tools.ietf.org/html/rfc7234#section-4.1
164             return $res unless ($self->cache_vary($res));
165              
166             my $cc = join('|',$res->header('Cache-Control')); # Since we only do string matching, this should be ok
167             if (defined($cc)) {
168             ## o the "no-store" cache directive (see Section 5.2) does not appear
169             ## in request or response header fields, and
170             return $res if ($cc =~ m/no-store|no-cache/); # TODO: Improve no-cache use
171             if ($self->is_shared) {
172             ## o the "private" response directive (see Section 5.2.2.6) does not
173             ## appear in the response, if the cache is shared, and
174             return $res if ($cc =~ m/private/);
175             ## o the Authorization header field (see Section 4.2 of [RFC7235]) does
176             ## not appear in the request, if the cache is shared, unless the
177             ## response explicitly allows it (see Section 3.2), and
178             if ($request->header('Authorization')) {
179             return $res unless ($cc =~ m/public|must-revalidate|s-maxage/);
180             }
181             }
182             ## o the response either:
183             ##
184             ## * contains an Expires header field (see Section 5.3), or
185             ## * contains a max-age response directive (see Section 5.2.2.8), or
186             # This is implemented in HTTP::Response, but it relates to the old RFC2616
187             # and doesn't support shared caches.
188             $expires_in = $res->freshness_lifetime(%{$self->heuristics_opts}) || 0;
189              
190             ## * contains a s-maxage response directive (see Section 5.2.2.9)
191             ## and the cache is shared, or
192              
193             if ($self->is_shared && ($cc =~ m/s-maxage\s*=\s*(\d+)/)) {
194             $expires_in = $1;
195             }
196              
197              
198              
199             ## * contains a Cache Control Extension (see Section 5.2.3) that
200             ## allows it to be cached, or
201             # TODO
202              
203             ## * has a status code that is defined as cacheable by default (see
204             ## Section 4.2.2), or
205             # TODO: We only do GET
206              
207             ## * contains a public response directive (see Section 5.2.2.5).
208             # We do not specifically deal with this
209              
210             }
211             if ($expires_in > 0) {
212             $self->cache->set($self->key, $res, { expires_in => $expires_in });
213             }
214             }
215             return $res;
216             }
217             };
218              
219             1;
220              
221             __END__
222              
223             =head1 LIMITATIONS
224              
225             Will only cache C<GET> requests, and only successful responses.
226              
227             The module does not validate and does not serve stale responses, even
228             when it would be allowed to do so. It nevertheless does most of
229             RFC7234.
230              
231             =head1 BUGS
232              
233             Please report any bugs to
234             L<https://github.com/kjetilk/p5-lwp-useragent-chicaching/issues>.
235              
236              
237             =head1 AUTHOR
238              
239             Kjetil Kjernsmo E<lt>kjetilk@cpan.orgE<gt>.
240              
241             =head1 ACKNOWLEDGEMENTS
242              
243             It was really nice looking at the code of L<LWP::UserAgent::WithCache>, when I wrote this.
244              
245             Thanks to Matt S. Trout for rewriting this to a Role.
246              
247             =head1 COPYRIGHT AND LICENCE
248              
249             This software is copyright (c) 2015 by Kjetil Kjernsmo.
250              
251             This is free software; you can redistribute it and/or modify it under
252             the same terms as the Perl 5 programming language system itself.
253              
254              
255