File Coverage

lib/CatalystX/Fastly/Role/Response.pm
Criterion Covered Total %
statement 9 9 100.0
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 12 12 100.0


line stmt bran cond sub pod time code
1             package CatalystX::Fastly::Role::Response;
2             $CatalystX::Fastly::Role::Response::VERSION = '0.05';
3 1     1   698629 use Moose::Role;
  1         1  
  1         7  
4 1     1   3390 use Carp;
  1         1  
  1         73  
5              
6 1         542 use constant CACHE_DURATION_CONVERSION => {
7             s => 1,
8             m => 60,
9             h => 3600,
10             d => 86_400,
11             M => 2_628_000,
12             y => 31_556_952,
13 1     1   4 };
  1         2  
14              
15             my $convert_string_to_seconds = sub {
16             my $input = $_[0];
17             my $measure = chop($input);
18              
19             my $unit = CACHE_DURATION_CONVERSION->{$measure} || #
20             carp
21             "Unknown duration unit: $measure, valid options are Xs, Xm, Xh, Xd, XM or Xy";
22              
23             carp "Initial duration start (currently: $input) must be an integer"
24             unless $input =~ /^\d+$/;
25              
26             return $unit * $input;
27             };
28              
29             =head1 NAME
30              
31             CatalystX::Fastly::Role::Response - Methods for Fastly intergration to Catalyst
32              
33             =head1 SYNOPTIS
34              
35             package MyApp;
36              
37             ...
38              
39             use Catalyst qw/
40             +CatalystX::Fastly::Role::Response
41             /;
42              
43             extends 'Catalyst';
44              
45             ...
46              
47             package MyApp::Controller::Root
48              
49             sub a_page :Path('some_page') {
50             my ( $self, $c ) = @_;
51              
52             $c->cdn_max_age('10d');
53             $c->browser_max_age('1d');
54              
55             $c->add_surrogate_key('FOO','WIBBLE');
56              
57             $c->response->body( 'Add cache and surrogate key headers' );
58             }
59              
60             =head1 DESCRIPTION
61              
62             This role adds methods to set appropreate cache headers in Catalyst responses,
63             relating to use of a Content Distribution Network (CDN) and/or Cacheing
64             proxy as well as cache settings for HTTP clients (e.g. web browser). It is
65             specifically targeted at L<Fastly|https://www.fastly.com> but may also be
66             useful to others.
67              
68             Values are converted and headers set in C<finalize_headers>. Headers
69             affected are:
70              
71             =over 4
72              
73             =item -
74              
75             Cache-Control: HTTP client (e.g. browser) and CDN (if Surrogate-Control not used) cache settings
76              
77             =item -
78              
79             Surrogate-Control: CDN only cache settings
80              
81             =item -
82              
83             Surrogate-Key: CDN only, can then later be used to purge content
84              
85             =item -
86              
87             Pragma: only set for for L<browser_never_cache>
88              
89             =item -
90              
91             Expires: only for L<browser_never_cache>
92              
93             =back
94              
95             =head1 TIME PERIOD FORMAT
96              
97             All time periods are expressed as: C<Xs>, C<Xm>, C<Xh>, C<Xd>, C<XM> or C<Xy>,
98             e.g. seconds, minutes, hours, days, months or years, e.g. C<3h> is three hours.
99              
100             =head1 CDN METHODS
101              
102             =head2 cdn_max_age
103              
104             $c->cdn_max_age( '1d' );
105              
106             Used to set B<max-age> in the B<Surrogate-Control> header, which CDN's use
107             to determine how long to cache for. B<If I<not> supplied the CDN will use the
108             B<Cache-Control> headers value> (as set by L</browser_max_age>).
109              
110             =cut
111              
112             has cdn_max_age => (
113             is => 'rw',
114             isa => 'Maybe[Str]',
115             );
116              
117             =head2 cdn_stale_while_revalidate
118              
119             $c->cdn_stale_while_revalidate('1y');
120              
121             Applied to B<Surrogate-Control> only when L</cdn_max_age> is set, this
122             informs the CDN how long to continue serving stale content from cache while
123             it is revalidating in the background.
124              
125             =cut
126              
127             has cdn_stale_while_revalidate => (
128             is => 'rw',
129             isa => 'Maybe[Str]',
130             );
131              
132             =head2 cdn_stale_if_error
133              
134             $c->cdn_stale_if_error('1y');
135              
136             Applied to B<Surrogate-Control> only when L</cdn_max_age> is set, this
137             informs the CDN how long to continue serving stale content from cache
138             if there is an error at the origin.
139              
140             =cut
141              
142             has cdn_stale_if_error => (
143             is => 'rw',
144             isa => 'Maybe[Str]',
145             );
146              
147             =head2 cdn_never_cache
148              
149             $c->cdn_never_cache(1);
150              
151             When true the B<Surrogate-Control> header will have a value of B<private>,
152             this forces Fastly (other CDN's may behave differently) to never cache the
153             results (even for multiple outstanding requests), no matter what other
154             options have been set.
155              
156             =cut
157              
158             has cdn_never_cache => (
159             is => 'rw',
160             isa => 'Bool',
161             default => sub {0},
162             );
163              
164             =head1 BROWSER METHODS
165              
166             =head2 browser_max_age
167              
168             $c->browser_max_age( '1m' );
169              
170             Used to set B<max-age> in the B<Cache-Control> header, browsers use this to
171             determine how long to cache for. B<The CDN will also use this if there is
172             no B<Surrogate-Control> (as set by L</cdn_max_age>)>.
173              
174             =cut
175              
176             has browser_max_age => (
177             is => 'rw',
178             isa => 'Maybe[Str]',
179             );
180              
181             =head2 browser_stale_while_revalidate
182              
183             $c->browser_stale_while_revalidate('1y');
184              
185             Applied to B<Cache-Control> only when L</browser_max_age> is set, this
186             informs the browser how long to continue serving stale content from cache while
187             it is revalidating from the CDN.
188              
189             =cut
190              
191             has browser_stale_while_revalidate => (
192             is => 'rw',
193             isa => 'Maybe[Str]',
194             );
195              
196             =head2 browser_stale_if_error
197              
198             $c->browser_stale_if_error('1y');
199              
200             Applied to B<Cache-Control> only when L</browser_max_age> is set, this
201             informs the browser how long to continue serving stale content from cache
202             if there is an error at the CDN.
203              
204             =cut
205              
206             has browser_stale_if_error => (
207             is => 'rw',
208             isa => 'Maybe[Str]',
209             );
210              
211             =head2 browser_never_cache
212              
213             $c->browser_never_cache(1);
214              
215             When true the headers below are set, this forces the browser to never cache
216             the results. B<private> is NOT added as this would also affect the CDN
217             even if C<cdn_max_age> was set.
218              
219             Cache-Control: no-cache, no-store, must-revalidate, max-age=0, max-stale=0, post-check=0, pre-check=0
220             Pragma: no-cache
221             Expires: 0
222              
223             N.b. Some versions of IE won't let you download files, such as a PDF if it is
224             not allowed to cache it, it is recommended to set a L</browser_max_age>('1m')
225             in this situation.
226              
227             IE8 have issues with the above and using the back button, and need an additional I<Vary: *> header,
228             L<as noted by Fastly|https://docs.fastly.com/guides/debugging/temporarily-disabling-caching>,
229             this is left for you to impliment.
230              
231             =cut
232              
233             has browser_never_cache => (
234             is => 'rw',
235             isa => 'Bool',
236             default => sub {0},
237             );
238              
239             =head1 SURROGATE KEYS
240              
241             =head2 add_surrogate_key
242              
243             $c->add_surrogate_key('FOO','WIBBLE');
244              
245             This can be called multiple times, the values will be set
246             as the B<Surrogate-Key> header as I<`FOO WIBBLE`>.
247              
248             See L<MooseX::Fastly::Role/cdn_purge_now> if you are
249             interested in purging these keys!
250              
251             =cut
252              
253             has _surrogate_keys => (
254             traits => ['Array'],
255             is => 'ro',
256             isa => 'ArrayRef[Str]',
257             default => sub { [] },
258             handles => {
259             add_surrogate_key => 'push',
260             has_surrogate_keys => 'count',
261             surrogate_keys => 'elements',
262             join_surrogate_keys => 'join',
263             },
264             );
265              
266             =head1 INTERNAL METHODS
267              
268             =head2 finalize_headers
269              
270             The method that actually sets all the headers, should be called
271             automatically by Catalyst.
272              
273             =cut
274              
275             before 'finalize_headers' => sub {
276             my $c = shift;
277              
278             if ( $c->browser_never_cache ) {
279              
280             $c->res->header( 'Cache-Control' =>
281             'no-cache, no-store, must-revalidate, max-age=0, max-stale=0, post-check=0, pre-check=0'
282             );
283             $c->res->header( 'Pragma' => 'no-cache' );
284             $c->res->header( 'Expires' => '0' );
285              
286             } elsif ( my $browser_max_age = $c->browser_max_age ) {
287              
288             my @cache_control;
289              
290             push @cache_control, sprintf 'max-age=%s',
291             $convert_string_to_seconds->($browser_max_age);
292              
293             if ( my $duration = $c->browser_stale_while_revalidate ) {
294             push @cache_control, sprintf 'stale-while-revalidate=%s',
295             $convert_string_to_seconds->($duration);
296              
297             }
298              
299             if ( my $duration = $c->browser_stale_if_error ) {
300             push @cache_control, sprintf 'stale-if-error=%s',
301             $convert_string_to_seconds->($duration);
302              
303             }
304              
305             $c->res->header( 'Cache-Control' => join( ', ', @cache_control ) );
306              
307             }
308              
309             # Set the caching at CDN, seperate to what the user's browser does
310             # https://docs.fastly.com/guides/tutorials/cache-control-tutorial
311             if ( $c->cdn_never_cache ) {
312              
313             # Make sure fastly doesn't cache this by accident
314             # tell them it's private, must be on the Cache-Control header
315             my $cc = $c->res->header('Cache-Control');
316             if ( $cc && $cc !~ /private/ ) {
317             $c->res->headers->push_header( 'Cache-Control' => 'private' );
318             }
319              
320             } elsif ( my $cdn_max_age = $c->cdn_max_age ) {
321              
322             my @surrogate_control;
323              
324             push @surrogate_control, sprintf 'max-age=%s',
325             $convert_string_to_seconds->($cdn_max_age);
326              
327             if ( my $duration = $c->cdn_stale_while_revalidate ) {
328             push @surrogate_control, sprintf 'stale-while-revalidate=%s',
329             $convert_string_to_seconds->($duration);
330              
331             }
332              
333             if ( my $duration = $c->cdn_stale_if_error ) {
334             push @surrogate_control, sprintf 'stale-if-error=%s',
335             $convert_string_to_seconds->($duration);
336              
337             }
338              
339             $c->res->header(
340             'Surrogate-Control' => join( ', ', @surrogate_control ) );
341              
342             }
343              
344             # Surrogate key
345             if ( $c->has_surrogate_keys ) {
346              
347             # See http://www.fastly.com/blog/surrogate-keys-part-1/
348             $c->res->header( 'Surrogate-Key' => $c->join_surrogate_keys(' ') );
349             }
350              
351             };
352              
353             =head1 SEE ALSO
354              
355             L<MooseX::Fastly::Role> - provides cdn_purge_now and access to L<Net::Fastly>
356             L<stale-while-validate|https://www.fastly.com/blog/stale-while-revalidate/>
357              
358             =head1 AUTHOR
359              
360             Leo Lapworth <LLAP@cpan.org>
361              
362             =cut
363              
364             1;