Running under Apache/mod_perl
Today we discuss some topics around running Catalyst applications in a Apache/mod_perl environment. (In fact used with Apache2/mod_perl2.)
Motivation
Why should you need to run Catalyst applications under Apache?
Several reasons:
- * Deliver static files
-
Sometimes delivering static files through the
Catalyst::Plugin::Static::Simple
is not enough.All your content is generated statically from a Content Management System (CMS) that does not know about your
root/static
subdir. The static files are scattered over many non-conforming places or you can't work out what they are just by file type. - * Some of your static files are "less static" than others
-
You might want to deliver images as really static files but have enriched some of your html files with template code and want to execute this through your preferred Catalyst::View.
- * Integrate your application into the same context of other applications on your server
-
You might have several applications that are already delivered through Apache. They together with your Catalyst app might all share the same environment, e.g. from a common authentication layer providing a singe sign-on.
- * Enhance a generally static website with some application pages
-
You want to mix only a few application URIs into your otherwise perfect static website, e.g. a newsletter subscription, a search function or some forms. You take URIs seriously - you want them anywhere, maybe right in the middle.
- * You want to use advanced Apache/mod_perl features
-
You might want to apply features like reverse proxies, mod_rewrite, output filtering or authentication/authorization to your applications, all at the same time and consecutive.
- * You want all of the above at once
-
Of course. I did.
Virtual host
All the following Apache configuration is assumed inside a virtual host:
<VirtualHost myapp.myhost.net> # ... </VirtualHost>
Configuration during Apache startup
Configuring things like environment variables, load path, etc. can be
tricky in a mod_perl environment because changing @INC
or %ENV
is not at any time possible. Calling a startup file helps. In Apache
config use:
PerlConfigRequire /home/renormalist/myapp/lib/MyApp/Config.pm
That file contains lines like
use lib '/home/renormalist/myapp/lib';
pointing to where MyApp
is installed and maybe other interesting
initialization stuff.
Static delivery
Define your website with a typical DocumentRoot
to deliver
everything through Apache by default:
DocumentRoot /home/renormalist/myapp/myappsite/
Deliver files through MyApp
Match html files of wanted subdirs through Catalyst with the
LocationMatch
directives and assign MyApp
as ResponseHandler:
<LocationMatch "/(start|news|blog)[-_\w\d/]*\.html$"> SetHandler modperl PerlResponseHandler MyApp </LocationMatch>
This way all the matched URIs are handled by MyApp.
In catalyst you need a controller that matches the same URIs and delivers the files, e.g.:
package MyApp::Controller::DeliverHtmlFiles; use base 'Catalyst::Controller::BindLex'; sub html_pages : Regex('^(start|news|blog)[-_\w\d/]+\.html$') { my ( $self, $c ) = @_; my $template : Stash = $c->request->path; }
The example is simplified. You need more code to handle nonexistent files or to handle URIs that end in dir names and need "/index.html" added.
Your view needs to know about your base path. I point Mason
to my
original application root/
and to the same DocumentRoot as
configured for Apache above.
package MyApp::View::Mason; use base 'Catalyst::View::Mason'; __PACKAGE__->config( comp_root => [ [ myapproot => MyApp->config->{root}.'' ], # stringify [ docroot => '/home/renormalist/myapp/myappsite' ], ] );
Deliver more URIs through MyApp
Additionally provide applications under app/
through Catalyst:
<LocationMatch "/app/.*"> SetHandler modperl PerlResponseHandler MyApp </LocationMatch>
To serve this URIs I use controllers below the
MyApp::Controller::App
namespace and a root/app/
directory for
its application templates:
package MyApp::Controller::App::News; use base 'Catalyst::Controller::BindLex'; sub daily : Local { my ($self, $c) = @_; # ... }
One for all
All the above ResponseHandlers are pointing to the very same single
application instance MyApp
. It is lazily started on the first
request to one of the matched URIs.
That's a specific feature of Catalyst::Engine::Apache
. If you want
more application instances in one Apache instance, then the mod_perl
engine probably isn't the right thing for you.
Application base
The Catalyst::Engine::Apache
tries to set your base path to the
place where your Location
or LocationMatch
points to, meaning
that all other paths are used relative to that. This is sometimes not
the Right Thing.
My html files all use links that are relative to /
(i.e., they are
not relative but absolute) and we cannot rewrite them all to use
uri_for()
because they come from a CMS that does things its own way
and we want to re-use the same files for static preview purposes.
A workaround is to not use Location
but LocationMatch
and make
its regex "non-trivial enough" so that deriving a single base path
from that Regex isn't possible, even when it is in fact a single
location:
<LocationMatch "/app/.*$"> # ... </LocationMatch>
Who doesn't like that dirty trick needs to overwrite
prepare_path
. (Or is there an easier way to do it?)
Take information from apache request into your application
Your application might not always run under Apache. E.g., during unit tests you might not have Apache specific request information or cookies from the browser available.
One possibility is to overwrite prepare_headers
to collect that
information if available and then access it engine-independent in the
rest of your application.
Here I mix information about the originally wanted uri and the
authentication reason (both resulting from my authentication layer
based on Apache2::AuthCookie
) into application request headers:
# MyApp.pm sub prepare_headers { my $self = shift; $self->NEXT::ACTUAL::prepare_headers(@_); if ($self->can('apache')) { if ($self->apache->prev) { $self->request->header( 'X-MyApp-Destination' => $self->apache->prev->uri ); $self->request->header( 'X-MyApp-AuthCookieReason' => $self->apache->prev->subprocess_env('AuthCookieReason') ); } } return; }
Chained actions, captured arguments and utf-8
There are some issues if you want to use chained actions and capture args as parts of the uri and these args contain non trivial values similar to the way wikipedia does it (e.g. http://en.wikipedia.org/wiki/Na%C3%AFve ).
Sooner or later you wonder how to allow slashes in arguments. Then remember this Apache option:
AllowEncodedSlashes On
To take such params as utf-8 it is neccessary to explicitely tag it as such:
sub prepare_foo : Chained('/') PathPrefix CaptureArgs(1) { my ($self, $c, $param_foo) = @_; Encode::_utf8_on($param_foo); # ... }
(Maybe that would be a worthy extension to
Catalyst::Plugin::Unicode
.)
Multi level templates for CMS purposes
Our DeliverHtmlFiles
controller above delivers all files through
the default view, e.g. Catalyst::View::Mason
. So you can augment
the .html files with Mason code to do dynamic stuff.
Unfortunately providing template code can be tricky if you already use the same template language in your CMS where you create those pages.
You could use two different template engines that don't interfere, e.g. Mason in your CMS templates that in turn contain TT2 code used during the Catalyst delivery. Or vice versa.
Alternatively you could escape template code to realize multi level templating. I escaped multi level Mason code with an alternative syntax, like
<!--metamason% my $MYAPP_UNAME = $c->req->header('MYAPP_UNAME'); --> Your login is: <!--% $MYAPP_UNAME %-->
and preprocess it in MyApp::View::Mason
:
__PACKAGE__->config( preprocess => sub { my $text = shift; # ref # <!--% ... %--> becomes <% ... %> $$text =~ s/<!--%/<%/g; $$text =~ s/%-->/%>/g; # <!--metamason% ... --> becomes % ... at linestart $$text =~ s,^\s*<!--\s*metamason%(.*?)-->,%$1,msg; } );
I saw something similar for Template Toolkit here (German, sorry):
That's it
Thanks to the people in #catalyst and especially to rafl for always having ideas and solutions to my problems.
Author
Steffen "renormalist" Schwigon
See also
- * http://bricolage.cc - Bricolage, statically publishing, enterprise-class CMS, written in Perl
- * http://modperl2book.org/ - mod_perl2 User's Guide Book.