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.

9 Comments

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?

"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!

Leave a comment

About Aristotle

user-pic Waxing philosophical