I use Hypnotoad behind nginx to run my Mojolicious applications. nginx
is better at handling TLS (than the flaky IO::Socket::SSL module), and
it can be configured to cache static resources. My first attempt to set
up caching was to set Expires in an after_static_dispatch hook,
but it took me a while to figure out the right combination of hooks to
do what I wanted.
This post describes how to use after_dispatch and after_static_dispatch
hooks to make sure static resources (i.e. public/*) are served with an
expiry date based on the content type (and without cookies), and also
to prevent some dynamic responses from being cached.
At first, I thought that after_static_dispatch would be called when the
static dispatcher served a response, and after_dispatch would be called
otherwise. But in fact, both hooks are always executed, and you
can't rely on one being called before the other. Instead, we must check
if a response code is already set when after_static_dispatch is called.
If so, we know the response came from the static dispatcher, and we can
add an Expires header based on the content type. If that header
is not present when after_dispatch is called, we know it's a dynamic
response, and can add a Cache-control header.
The static dispatcher does not set the content type when serving a "304
Not modified" response (to clients that used If-Modified-Since), so we
can't rely on the type being available in after_static_dispatch.
Here's an example. The following code should be in your lib/App.pm (in
sub startup). Note the type-specific expiry and caching directives in
the second hook.
$app->hook(after_dispatch => sub {
my $tx = shift;
# Was the response dynamic?
return if $tx->res->headers->header('Expires');
# If so, try to prevent caching
$tx->res->headers->header(
Expires => Mojo::Date->new(time-365*86400)
);
$tx->res->headers->header(
"Cache-Control" => "max-age=1, no-cache"
);
});
$app->hook(after_static_dispatch => sub {
my $tx = shift;
my $code = $tx->res->code;
my $type = $tx->res->headers->content_type;
# Was the response static?
return unless $code && ($code == 304 || $type);
# If so, remove cookies and/or caching instructions
$tx->res->headers->remove('Cache-Control');
$tx->res->headers->remove('Set-Cookie');
# Decide on an expiry date
my $e = Mojo::Date->new(time+600);
if ($type) {
if ($type =~ /javascript/) {
$e = Mojo::Date->new(time+300);
}
elsif ($type =~ /^text\/css/ || $type =~ /^image\//) {
$e = Mojo::Date->new(time+3600);
$tx->res->headers->header("Cache-Control" => "public");
}
# …other conditions…
}
$tx->res->headers->header(Expires => $e);
});
The example above marks all dynamic responses as uncacheable.
This may be overkill. Special cases can be handled either by teaching
after_dispatch to identify and exempt certain responses ($tx->res),
or by setting Expires by hand (because the hook skips responses
that already have an expiry time set).
I have "Export to XLS" links at various places, and I use the excellent
Spreadsheet::WriteExcel
module to render a spreadsheet into (a filehandle opened on) a scalar,
and call «render_data($data, 'xls')» to send the data. Since
there are many such instances and they all use the same content type, I
can identify them easily in after_dispatch.
$app->hook(after_dispatch => sub {
my $tx = shift;
# Was the response dynamic?
return if $tx->res->headers->header('Expires');
# Allow spreadsheets to be cached
my $type = $tx->res->headers->content_type;
if ($type eq 'application/vnd.ms-excel') {
$tx->res->headers->header(
Expires => Mojo::Date->new(time+86400)
);
return;
}
…
Aside: Call «$app->types->type(xls =>
'application/vnd.ms-excel')» in startup to register 'xls' as a
content type.
In some places, I call render_static() to render an existing file, whose
MIME type can't be predicted in advance. In this case, it's easier to
set Expires by hand.
$self->render_static("/path/to/file");
$self->res->headers->content_type("some/type");
$self->res->headers->content_disposition(
"attachment; filename=\"file.name\""
);
$self->res->headers->header(
Expires => Mojo::Date->new(time+86400)
);
For more information about caching, read Mark Nottingham's
Caching tutorial for web
authors and webmasters.