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.07';
3 1     1   896798 use Moose::Role;
  1         3  
  1         9  
4 1     1   4426 use Carp;
  1         1  
  1         96  
5              
6 1         690 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   7 };
  1         1  
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 a B<private> will be added to the B<Cache-Control> header
152             this forces Fastly to never cache the results, no matter what other
153             options have been set.
154              
155             =cut
156              
157             has cdn_never_cache => (
158             is => 'rw',
159             isa => 'Bool',
160             default => sub {0},
161             );
162              
163             =head1 BROWSER METHODS
164              
165             =head2 browser_max_age
166              
167             $c->browser_max_age( '1m' );
168              
169             Used to set B<max-age> in the B<Cache-Control> header, browsers use this to
170             determine how long to cache for. B<The CDN will also use this if there is
171             no B<Surrogate-Control> (as set by L</cdn_max_age>)>.
172              
173             =cut
174              
175             has browser_max_age => (
176             is => 'rw',
177             isa => 'Maybe[Str]',
178             );
179              
180             =head2 browser_stale_while_revalidate
181              
182             $c->browser_stale_while_revalidate('1y');
183              
184             Applied to B<Cache-Control> only when L</browser_max_age> is set, this
185             informs the browser how long to continue serving stale content from cache while
186             it is revalidating from the CDN.
187              
188             =cut
189              
190             has browser_stale_while_revalidate => (
191             is => 'rw',
192             isa => 'Maybe[Str]',
193             );
194              
195             =head2 browser_stale_if_error
196              
197             $c->browser_stale_if_error('1y');
198              
199             Applied to B<Cache-Control> only when L</browser_max_age> is set, this
200             informs the browser how long to continue serving stale content from cache
201             if there is an error at the CDN.
202              
203             =cut
204              
205             has browser_stale_if_error => (
206             is => 'rw',
207             isa => 'Maybe[Str]',
208             );
209              
210             =head2 browser_never_cache
211              
212             $c->browser_never_cache(1);
213              
214             When true the headers below are set, this forces the browser to never cache
215             the results. B<private> is NOT added as this would also affect the CDN
216             even if C<cdn_max_age> was set.
217              
218             Cache-Control: no-cache, no-store, must-revalidate, max-age=0, max-stale=0, post-check=0, pre-check=0
219             Pragma: no-cache
220             Expires: 0
221              
222             N.b. Some versions of IE won't let you download files, such as a PDF if it is
223             not allowed to cache it, it is recommended to set a L</browser_max_age>('1m')
224             in this situation.
225              
226             IE8 have issues with the above and using the back button, and need an additional I<Vary: *> header,
227             L<as noted by Fastly|https://docs.fastly.com/guides/debugging/temporarily-disabling-caching>,
228             this is left for you to impliment.
229              
230             =cut
231              
232             has browser_never_cache => (
233             is => 'rw',
234             isa => 'Bool',
235             default => sub {0},
236             );
237              
238             =head1 SURROGATE KEYS
239              
240             =head2 add_surrogate_key
241              
242             $c->add_surrogate_key('FOO','WIBBLE');
243              
244             This can be called multiple times, the values will be set
245             as the B<Surrogate-Key> header as I<`FOO WIBBLE`>.
246              
247             See L<MooseX::Fastly::Role/cdn_purge_now> if you are
248             interested in purging these keys!
249              
250             =cut
251              
252             has _surrogate_keys => (
253             traits => ['Array'],
254             is => 'ro',
255             isa => 'ArrayRef[Str]',
256             default => sub { [] },
257             handles => {
258             add_surrogate_key => 'push',
259             has_surrogate_keys => 'count',
260             surrogate_keys => 'elements',
261             join_surrogate_keys => 'join',
262             },
263             );
264              
265             =head1 INTERNAL METHODS
266              
267             =head2 finalize_headers
268              
269             The method that actually sets all the headers, should be called
270             automatically by Catalyst.
271              
272             =cut
273              
274             before 'finalize_headers' => sub {
275             my $c = shift;
276              
277             if ( $c->browser_never_cache ) {
278              
279             $c->res->header( 'Cache-Control' =>
280             'no-cache, no-store, must-revalidate, max-age=0, max-stale=0, post-check=0, pre-check=0'
281             );
282             $c->res->header( 'Pragma' => 'no-cache' );
283             $c->res->header( 'Expires' => '0' );
284              
285             } elsif ( my $browser_max_age = $c->browser_max_age ) {
286              
287             my @cache_control;
288              
289             push @cache_control, sprintf 'max-age=%s',
290             $convert_string_to_seconds->($browser_max_age);
291              
292             if ( my $duration = $c->browser_stale_while_revalidate ) {
293             push @cache_control, sprintf 'stale-while-revalidate=%s',
294             $convert_string_to_seconds->($duration);
295              
296             }
297              
298             if ( my $duration = $c->browser_stale_if_error ) {
299             push @cache_control, sprintf 'stale-if-error=%s',
300             $convert_string_to_seconds->($duration);
301              
302             }
303              
304             $c->res->header( 'Cache-Control' => join( ', ', @cache_control ) );
305              
306             }
307              
308             # Set the caching at CDN, seperate to what the user's browser does
309             # https://docs.fastly.com/guides/tutorials/cache-control-tutorial
310             if ( $c->cdn_never_cache ) {
311              
312             # Make sure fastly doesn't cache this by accident
313             # tell them it's private, must be on the Cache-Control header
314             my $cc = $c->res->header('Cache-Control') || '';
315             if ( $cc !~ /private/ ) {
316             $c->res->headers->push_header( 'Cache-Control' => 'private' );
317             }
318              
319             } elsif ( my $cdn_max_age = $c->cdn_max_age ) {
320              
321             my @surrogate_control;
322              
323             push @surrogate_control, sprintf 'max-age=%s',
324             $convert_string_to_seconds->($cdn_max_age);
325              
326             if ( my $duration = $c->cdn_stale_while_revalidate ) {
327             push @surrogate_control, sprintf 'stale-while-revalidate=%s',
328             $convert_string_to_seconds->($duration);
329              
330             }
331              
332             if ( my $duration = $c->cdn_stale_if_error ) {
333             push @surrogate_control, sprintf 'stale-if-error=%s',
334             $convert_string_to_seconds->($duration);
335              
336             }
337              
338             $c->res->header(
339             'Surrogate-Control' => join( ', ', @surrogate_control ) );
340              
341             }
342              
343             # Surrogate key
344             if ( $c->has_surrogate_keys ) {
345              
346             # See http://www.fastly.com/blog/surrogate-keys-part-1/
347             $c->res->header( 'Surrogate-Key' => $c->join_surrogate_keys(' ') );
348             }
349              
350             };
351              
352             =head1 SEE ALSO
353              
354             L<MooseX::Fastly::Role> - provides cdn_purge_now and access to L<Net::Fastly>
355             L<stale-while-validate|https://www.fastly.com/blog/stale-while-revalidate/>
356              
357             =head1 AUTHOR
358              
359             Leo Lapworth <LLAP@cpan.org>
360              
361             =cut
362              
363             1;