I\'m looking for a reliable way to configure Mojolicious running behind an Apache reverse proxy under /app, so that url_for(\'/foo\')
actually returns /ap
You need to set base path for each request url in before_dispatch hook
$app->hook(before_dispatch => sub {
my $c = shift;
$c->req->url->base->path('/app/');
});
Example:
use Mojolicious::Lite;
app->hook(before_dispatch => sub {
shift->req->url->base->path('/app/');
});
get '/' => sub {
my $c = shift;
$c->render(text => $c->url_for('test'));
};
get '/test/url' => sub { ... } => 'test';
app->start;
And result:
$ curl 127.0.0.1:3000
/app/test/url
You should mount your application under required path.
In your startup
you should do:
$r = $app->routes;
$r = $r->any( '/api' )->partial( 1 );
$r->get( '/test' );
You should not configure specially your apache. When GET /api/test
will come your app will get /api/test
route. This route partially matched to /api
and the rest route /test
will be assigned into ->stash->{ path }
.
So rest routes will be checked against /test
(source)
I'm now answering my own question as I'm getting more suggestions (not just here) from people who would put a hard-coded prefix in their application code. It should be obvious that prefixing all generated urls manually isn't a solution. Just imagine two instances of the same application deployed on the same server, one under /app1
and the other under /app2
. The whole point of the suggested code in my question is that the application works and produces correct urls if accessed through a reverse proxy without breaking requests going directly to the application server. A developer would run Morbo, but a hard-coded prefix would break that. Also, I made at least one mistake in my question, but nobody seems to have noticed.
I had too many slashes in my example in the question.
The way the Location
block is defined, requests to /app
without trailing slash would fail. It might be better to write it like that:
<Location "/app">
...
Next, I wrote that I check for the X-Forwarded-For
header but I actually checked for X-Forwarded-Host
. That wouldn't be a problem if I were also clearing that header but I cleared X-Forwarded-For
instead. With that awkward mistake, the safety mechanism wouldn't work, so if you'd set this header while working with your application server at localhost:3000, the app would try to repair the manipulated url even though it's not supposed to do that.
It should've been:
RequestHeader unset X-Forwarded-Host
Example:
ProxyPreserveHost On
<Location /app>
ProxyPass http://localhost:3000/app
RequestHeader unset X-Forwarded-Host
</Location>
The ProxyPreserveHost
directive isn't required as long as the application uses relative urls everywhere. If the application wants to generate an absolute url, for example url_for('/page')->to_abs
, ProxyPreserveHost
should be enabled, otherwise external clients would get http://localhost:3000/app/page
.
When I wrote that question, I saw the before_dispatch
hook in the Mojolicious documentation and, as pointed out in the question, I wanted to use it for an application running under /app
. However, I didn't want to break Morbo. The example assumes that the application is in production mode ($app->mode
) while running behind a reverse proxy but not when access directly through Morbo, but I didn't want to change the mode for every other request.
That's why I added a condition to check if the request came through a reverse proxy. As this header is only set by Apache (by the mod_proxy_http module) and not by Mojo::Server::Morbo
, it can be used as reverse proxy detection. Together with the right directive to clear the X-Forwarded-Host
, I believe the answer to my question would be that yes, that should work reliably.
(Although that last part isn't strictly necessary as long as direct access to the app server is limited to the developer.)
To show why I've added the /app
prefix to the ProxyPass
line in the Apache configuration, I'd like to point out that this approach manipulates the url to allow the application to work under the given prefix. There's another question of someone who forgot to add the prefix in the Apache configuration and I wrote an answer explaining what the hook does.
Morbo: localhost:3000
Apache reverse proxy: host.com/app or localhost/app
# Browser > Apache:
http://host.com/app
# Apache (ProxyPass http://localhost:3000/) > Mojolicious sees:
GET /
url_for '/test' = /test
(or even //test if the hook pushes undef, see the other answer linked above)
# Apache (configured as described above) > Mojolicious sees:
GET /app
# Hook:
base = /app
request = /
url_for '/test' = /app/test
Normally, the local target argument in the ProxyPass
directive would not have the prefix, it would just be something like ProxyPass http://...:3000/
. In that case, the application doesn't know about the prefix, which is why all generated urls and links are incomplete.
This approach requires you to let Apache pass the prefix through to the application server. The application doesn't know about the prefix, so it wouldn't know what to do with a request to /app/page
. This is where the hook comes in. It assumes that the first level of the path is always the prefix, so it turns /app/page
into /page
and it conveniently appends the /app
prefix to the url base, which is used when generating urls, making sure that a link to /test
actually points to /app/test
.
Obviously, this modification should not be done for any request sent directly to Morbo.
Alternatively, a custom request header could be set by the reverse proxy and then used by the hook to produce working urls. The Mojolicious::Plugin::RequestBase module works that way. It expects you to define the prefix in the X-Request-Base header, not in the url:
RequestHeader set X-Request-Base "/app"
In that case, the application should only receive requests for urls relative to that prefix:
ProxyPass http://localhost:3000/
All that module really does is pick up the header and use it as url base:
$c->req->url->base($url); # url = X-Request-Base = /app
Example:
<Location /app>
ProxyPass http://localhost:3000
RequestHeader set X-Request-Base "/app"
</Location>
This is a nice and simply solution. Note that the /app
prefix appears twice in that case. And of course, the hook implemented by that module only does its work if the X-Request-Base
header is set, just like the hook shown above does nothing if the X-Forwarded-Host
header is not set.