It's extremely common for a Mojolicious web application to be hosted behind some kind of HTTP proxy: A production website usually includes Varnish, or Nginx, or a CDN (probably using Varnish or Nginx).

In the most common case, a web application is the entire domain, so configuring the reverse proxy is very simple: Add the -p option to hypnotoad or myapp.pl daemon command, or set the MOJO_REVERSE_PROXY environment variable to a true value. See the Mojolicious Cookbook for more details.

But what if my application doesn't have its own domain? How do I host a Mojolicious application as a reverse proxy from a path in another domain?

Hosting a site as a reverse proxy from a path in a domain allows a bunch of individual applications to handle a single domain. Static content can be served by a simple server like Apache HTTPD, and a section of dynamic content can be a Mojolicious application.

This presents a problem: If I host a blog application at http://example.com/blog, every URL in the application needs to start with /blog. I could fix this by adding the prefix to every link in every template and all the content in my application, but this creates some problems. First, since the /blog path is added by my proxy, trying to run the application locally for testing will break all the links (unless I also run a local proxy). Second, if I want to change the path, I need to change all my templates!

There's an easier way. Every request in Mojolicious has a URL, a Mojo::URL object. Every Mojo::URL object has a base. When Mojolicious parses a request, it takes the request's path (/blog) and puts it in the URL. Then it takes the request's domain name (from the HTTP Host header) and puts it in the base. Then, every time the app needs an absolute URL (like in the url_for helper), it combines the URL with the base.

So, to solve the problem of hosting a Mojolicious app behind a proxy with a path, I need to add my proxy's path to the base of the request's URL. I can do that with the before_dispatch hook:

app->hook( before_dispatch => sub {
    my ( $c ) = @_;
    my $url = $c->req->url;
    my $base = $url->base;
    # Append "/blog" to the base path
    push @{ $base->path }, 'blog';
    # The base must end with a slash, making
    # http://example.com/blog/
    $base->path->trailing_slash(1);
    # and the URL must not, so that relative links 
    # on the home page work correctly
    $url->path->leading_slash(0);
});

Except now I have the same problem as if I just added /blog to every link in my templates: My URLs always have /blog in front, even when I'm running my application locally without a proxy. What I need is a way to configure the path in the production environment, but not in the development environment.

I could use the Config plugin and different configuration files for production and development to enable and disable this behavior as needed, but I already have a way to detect if the application behind a reverse proxy: The MOJO_REVERSE_PROXY environment variable. If that's set, I want to add the hook.

But I also have that magic string "blog" hanging out in our hook. This means the application knows what the reverse proxy's path is, and if the reverse proxy's path changes, so must the app. That's not good encapsulation. It'd be better if I put the proxy path inside the MOJO_REVERSE_PROXY environment variable. That way the hook could see if it exists, and then use it to fix the base path if it does!

if ( my $path = $ENV{MOJO_REVERSE_PROXY} ) {
    my @path_parts = grep /\S/, split m{/}, $path;
    app->hook( before_dispatch => sub {
        my ( $c ) = @_;
        my $url = $c->req->url;
        my $base = $url->base;
        push @{ $base->path }, @path_parts;
        $base->path->trailing_slash(1);
        $url->path->leading_slash(0);
    });
}

Now I can add MOJO_REVERSE_PROXY=/blog to my environment and my application hosted behind my reverse proxy will work correctly! Without the environment variable, my application still works correctly. As long as I make sure to use the url_for helper (either directly or via Mojolicious tag helpers), my application will work the same in both places.

There are lots of different ways that reverse proxies can be configured, so you may have to do something slightly different in your hook. See the Rewriting section of the Mojolicious Cookbook for other examples.

Original artwork by Doug Bell (licensed CC-BY-SA 4.0).

Tagged in : deployment, development

author image
Doug Bell

Doug (preaction) is a long time Perl user. He is the current maintainer of CPAN Testers and the author of many CPAN modules including the Statocles blog engine that powers this site.