Some nifty things you can do with Catalyst on Plack
Catalyst 5.9 is coming up, and the big change is a wholesale switch to PSGI, completely dropping Catalyst’s own engine system (outside of compatibility shims). This is bound to bring in new people wondering what PSGI and Plack are all about and what you can do with them.
I’ve been using Catalyst on top of Plack for a while now (with the aid of Catalyst::Engine::PSGI) and have gotten around to some nifty things with it. So here is my app.psgi
for inspiration:
#!/usr/bin/env perl
use strict;
use MyApp;
use Plack::Builder;
use Plack::App::Cascade;
use Plack::App::File;
use Plack::Util;
MyApp->setup_engine( 'PSGI' );
my $fingerprint_rx = qr/(?:\A|[;&])h=[A-Za-z0-9_]+(?=[;&]|\z)/;
my $static = builder {
enable_if { $_[0]{'QUERY_STRING'} =~ s!$fingerprint_rx!! } 'NeverExpire';
enable 'Precompressed', match => qr/\.js\z/;
Plack::App::File->new( root => MyApp->path_to( 'www' ) )->to_app;
};
my $dynamic = builder {
enable 'Deflater';
sub { MyApp->run( @_ ) };
};
my $app = do {
my $cas = Plack::App::Cascade->new;
$cas->add( $static );
$cas->add( $dynamic );
$cas->to_app;
};
builder {
enable 'UseChromeFrame';
enable 'NoFavicon';
$app;
};
All the middlewares except for Deflater are ones I wrote. Originally, they were part of the application. Since all of the functionality is applicable outside this project, I extracted them after the switch to Plack. They aren’t released yet, but only because none of them have tests.
You can also discern that I’m no longer using Catalyst::Plugin::Static::Simple. Instead I set up a Plack::App::Cascade that first tries to serve a request using Plack::App::File, then passes to the Catalyst app. So the Catalyst dispatcher never even gets to see or deal with the requests for static files.
The “Precompressed” middleware is a simple bit of kit that will try to serve a .gz
version of a static file if the browser supports compression. I have a build script that creates .gz
versions for all of our .js
files. So there’s no on-the-fly compression of static files.
“NeverExpire” sets Expire
and Cache-Control
headers one year into the future, and is mostly intended to be used with enable_if
– in this case, if a request for a static file included a content hash as a query parameter. (I have a uri_for_static
in our Catalyst app that will calculate and cache hashes for static files the first time they’re requested after startup, and append the hash as a query parameter to generated URIs. That way, browsers will immediately re-download eg. changed CSS files (because the URI has changed, because the hash query parameter has changed) but cache them effectively forever as long as they haven’t changed. I may release this as a Catalyst::Plugin at some point.)
“NoFavicon” intercepts favicon.ico
requests and sends back a 204 response. That way nothing further down inside the machinery ever gets to deal with these requests, esp. not the Catalyst dispatcher, which also keeps them from clogging the logs.
Finally, “UseChromeFrame” injects the response header necessary to trigger Google Chrome Frame if it sees the request header that signals the presence of Chrome Frame.
The other nice thing about it is that your extensions and tricks are reusable in all Plack based frameworks.
FYI, It'll be Catalyst 5.9 when it's released :)
Also, there is now some documentation on converting current uses of ::Engine::PSGI to the new version (in trunk) - your .psgi above should still run / work as expected, but there are a couple of tweaks to make it 'native'. This will be worked on further and announced with the next TRIAL release.
I'm interested in your middlewares.
And thanks for the informative post. I especially like the static part.
Nice article, Aristotle. I'm inspired to have a hack on this now!
I really like your style of not forwarding irrelevant requests on to Catalyst!
I've never liked the idea of a query parameter to control caching, but not having done a lot of work with caching myself I'm no expert by any means. :) Couldn't you just be sending Last-Modified or the ETag?
No.
With
Last-Modified
and/orETag
, the browser will include the information you need to be able to send a 304 response with no entity body when asking for these resources again (which is good for periodically changing resources such as feeds).But with an
Expires
header in the far future, the browser won’t even ask again until that much time has passed. It reduces the per-page overhead for these resources from a tiny linear one to zero. It also helps client-side page load speed because the browser never needs to round-trip over the network for these resources (which takes an eternity even if it’s just a single packet on both ends) before it can render the page. It also means we are no longer dependent on the browser’s cache revalidation to notice when CSS files change (which leads to “please do a hard refresh” support requests), because the URI changes as soon as the file does, so the browser will re-download.I’ve considered putting the hash into the path segment of the URI. I may yet do that. Really the only reason to do it this way was how much messier it is to tweak the path in
uri_for_static
rather than the query parameters."the only reason to do it this way" - I think there might be another good reason. Take this example-
/js/efa92b/file.js v /js/file.js?efa92b
The first is ephemeral. The second is likely to continue to resolve to something sensible. End users may copy the first URI to find it broken later or even immediately. Robots may also continue to look for it and put pointless 404s in your logs for years because it got stashed in some dump file somewhere.
So, your approach seems ideal. Please, please get any of your generalizable middlewares out to the CPAN. I'm sure many of us would love to use or just examine them for inspiration.
Whoops. Asked for your middlewares too soon. I mean late. I mean I see the ones discussed are on the CPAN already. What I get for jumping in a thread this late. Thanks!
Hmm, I meant to write an entry to mention that I had pushed them out to CPAN. It looks like I never got around to that at all. In any case, the code is out there. (Insert X-Files theme here.)