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):

* http://www.perl-uwe.de/blog/2007-03/template-im-template-teil-2/

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

http://renormalist.net

See also

* http://bricolage.cc - Bricolage, statically publishing, enterprise-class CMS, written in Perl
* http://modperl2book.org/ - mod_perl2 User's Guide Book.