Search code examples
perlnginxreverse-proxymojolicious

how to make Mojolicious's url_for()->to_abs() return correct scheme (http or https) behind nginx


Here is my situation:

1) Nginx listens both 80 and 443 port and forwards a request to Mojolicious app.

upstream ssltest {
    server 127.0.0.1:9000;
}

server {
    listen 80;
    listen 443 ssl;

    server_name ssltest.server.com;

    location / {
        proxy_pass http://ssltest/;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

2) In the Mojolicious app, I put this code:

get '/' => sub {
  my $c = shift;

  warn $c->url_for('/somepath')->to_abs;  # HERE

  $c->render(template => 'index');
};

I want that url_for('/somepath')->to_abs returns https://ssltest.server.com/somepath or http://.../somepath, depending on which protocol I use in my browser. But it always returns http

(There is some reason that I have to create an absolute path, rather than relative path)

So, I've tried to search in web.

https://groups.google.com/d/msg/mojolicious/zL-c4Tx1vCk/_ihsLRsNAgAJ

This link taught me I can pass the current protocol(http or https) to the Mojo app via a custom header X-Forward-Proto. However, url_for() still returned only http.

Then I tried such code:

warn $c->url_for('/somepath')->to_abs->scheme(
  $c->req->headers->header('X-Forwarded-Proto')
);

It worked! But now I have to modify every occurrences of url_for->to_abs in the code.

Next, I found this link: https://stackoverflow.com/a/27834805/1150133 which uses a hook to modify $c->req->url->base itself. I tried to apply it:

# change 'scheme' of request url
hook 'before_dispatch' => sub {
  my $c = shift;
  $c->req->url->base->scheme( $c->req->headers->header('X-Forwarded-Proto'));
};

get '/' => sub {
  my $c = shift;

  # url_for() seems to refer the request url to determine new url
  warn $c->url_for('/somepath')->to_abs;

  $c->render(template => 'index');
};

It worked very well.

Now I want to know whether this is a correct way or not. Is there any problem that would occur? Is there a better or the best way to acquire my goal?

Any advice would be appreciated.


Solution

  • Yes, that's a good way, but I would make it more flexible.

    First, there is never a correct way in Perl. Mojolicious is just Perl, too. There is more than one way to do it. Sometimes one or more ways are more correct than others. This is such a case.

    Your code is fine and will get the job done. However, if you decide to deploy the application in a different way, and that header goes away, things go boom. So you should at least check for its existence.

    # change 'scheme' of request url
    hook 'before_dispatch' => sub {
      my $c = shift;
      $c->req->url->base->scheme( $c->req->headers->header('X-Forwarded-Proto'))
        if $c->req->headers->header('X-Forwarded-Proto');
    };
    

    Now if you run it locally with morbo and you don't have the header set, things should still work (assuming they broke in the first place, which sounds like a reasonable assumption I did not verify).

    To see how to write unit tests for it, please refer to this recent question. It comes at the problem from a different side, but applies exactly.