Tornado & Apache reverse proxy can't get the right configuration

I am using apache to reverse proxy several app and expose only 1 port and 1 ssl certificat. I have manage to make it works for most of the apps but all webapp based on tornado can't follow the reverse proxy addresses.

Let's take an example: I run my proxy and tornado on the same localhost, port 64646 for the proxy 8080 for tornado app.
I want to have my tornado app on address like that http://localhost:64646/tornado/

The app is working flawless if we directly connect to it. For example like this : http://localhost:8080/web/

If I use the proxy setting and I type http://localhost:64646/tornado/ I should access the tornado webapp but something break between the webapp and apache and I am redirected to http://localhost:64646/web/auth/login/?next=%2F

The rewriting of URL rule "tornado" part is missing on the address and I can't access the page. That is normal because I am not at the right place. Also any of static files like css, seems to have the root of the server proxy as reference and not the relative "Tornado" proxy part.

Similar issue if I enter the full correct address for login : http://localhost:64646/tornado/auth/login/

The page correctly load but after the authentification I am redirected to

http://localhost:64646/web/index.html instead of http://localhost:64646/tornado/index.html

I certainly miss a stupid configuration but I can't find my mistake. All other webapp are working with my proxy setting only the one made with tornado have this kind of issue.

I can reproduce the issue using the very basic tornado login example and the following apache configuration

webserver.py

import tornado.auth
import tornado.escape
import tornado.httpserver
import tornado.ioloop
import tornado.options
import tornado.web
import Settings
import json
import os

from tornado.options import define, options

define("port", default=8080, help="run on the given port", type=int)

class BaseHandler(tornado.web.RequestHandler):
    def get_current_user(self):
        return self.get_secure_cookie("user")

class MainHandler(BaseHandler):
    @tornado.web.authenticated
    def get(self):
        params = {
            "title": "proxy test app"
        }
        self.render('index.html', **params)

class AuthLoginHandler(BaseHandler):
    def get(self):
        params = {
            "errormessage": self.get_argument("error", ''),
            "title": "proxy test app"
        }
        self.render('login.html', **params)

    def post(self):
        username = self.get_argument("username", "")
        password = self.get_argument("password", "")
        auth = (username == "admin" and password == "admin")
        if auth:
            self.set_current_user(username)
            self.redirect(self.get_argument("next", u"/web/"))
        else:
            error_msg = u"?error=" + tornado.escape.url_escape("Login incorrect")
            self.redirect(u"/web/auth/login/" + error_msg)

    def set_current_user(self, user):
        if user:
            self.set_secure_cookie("user", tornado.escape.json_encode(user))
        else:
            self.clear_cookie("user")

class AuthLogoutHandler(BaseHandler):
    def get(self):
        self.clear_cookie("user")
        self.redirect(self.get_argument("next", "/web/"))

class Application(tornado.web.Application):
    def __init__(self):
        handlers = [
            (r"/web/", MainHandler),
            (r"/web/auth/login/", AuthLoginHandler),
            (r"/web/auth/logout/", AuthLogoutHandler),
        ]
        settings = {
            "template_path":Settings.TEMPLATE_PATH,
            "static_path":Settings.STATIC_PATH,
            "debug":Settings.DEBUG,
            "cookie_secret": Settings.COOKIE_SECRET,
            "login_url": "/web/auth/login/"
        }
        tornado.web.Application.__init__(self, handlers, **settings)

def main():
    tornado.options.parse_command_line()
    http_server = tornado.httpserver.HTTPServer(Application())
    http_server.listen(options.port)
    tornado.ioloop.IOLoop.instance().start()

if __name__ == "__main__":
    main()

Settings.py

import os
DEBUG = True
DIRNAME = os.path.dirname(__file__)
STATIC_PATH = os.path.join(DIRNAME, 'static')
TEMPLATE_PATH = os.path.join(DIRNAME, 'template')

import logging
import sys
#log linked to the standard error stream
logging.basicConfig(level=logging.DEBUG,
                    format='%(asctime)s - %(levelname)-8s - %(message)s',
                    datefmt='%d/%m/%Y %Hh%Mm%Ss')
console = logging.StreamHandler(sys.stderr)

/template/login.html

<html>
   <head>
       <title>{{ title }}</title>
   </head>
   <body>
        <div id="formContent">
              <!-- Tabs Titles -->
              <!-- Icon -->
              <!-- Login Form -->
              <form action="/web/auth/login/" method="post" id="login_form">
                <input type="text" id="username"  name="username" placeholder="username">
                <input type="text" id="password"  name="password" placeholder="password">
                <input type="submit" value="Log In">
              </form>
              <div >
              <h3 class="active"> {{ errormessage}} </h3>
              </div>
            </div>
   </body>
 </html>

/template/index.html

<html>
   <head>
      <title>{{ title }}</title>
   </head>
   <body>
              <h3 class="active"> huho ? the droid you are looking for is not here ,,, </h3>
   </body>
 </html>

httpd.conf /apache configuration

<VirtualHost *:64646>
    ServerAdmin [email protected]
    DocumentRoot "C:/Users\Administrator/Desktop/"
    ServerName proxy-test.com   
    ErrorLog  "|bin/rotatelogs.exe -n 2 logs/https_proxy_error_log.txt 84600 50M"  
    CustomLog "|bin/rotatelogs.exe -n 2 logs/https_proxy_access.txt 84600 50M" common     
    ProxyHTMLEnable On   
    ProxyPreserveHost On 
    ProxyRequests on   
    #SSLEngine on
    #SSLProxyEngine On
    SSLProxyVerify none 
    SSLProxyCheckPeerCN off
    SSLProxyCheckPeerName off
    SSLProxyCheckPeerExpire off
        
    #SSLProtocol -all +TLSv1.2
    #SSLCertificateFile "mycrtfile.crt" 
    #SSLCertificateKeyFile "mykeyfile.key" 
    RewriteEngine on
    RewriteCond %{REQUEST_METHOD} ^TRACE
    RewriteRule .* - [F]
    
    ProxyPassMatch    "/otherapp2/(.*)" "http://localhost:30000/$1"
    ProxyPassReverse  "/otherapp2/" "http://localhost:30000/"

    ProxyPassMatch    "/otherapp/(.*)" "http://localhost:8888/$1"
    ProxyPassReverse  "/otherapp/"    "http://localhost:8888"
   
    ProxyPassMatch    "/tornado/(.*)" "http://localhost:8080/web/$1"
    ProxyPassReverse  "/tornado/" "http://localhost:8080/web/"
    
    <location "/otherapp/">
        ProxyHTMLURLMap / /otherapp/
     Order allow,deny
     Allow from all
    </location>
</VirtualHost>

Your Tornado app is sending this redirect - /auth/login. But your Apache server doesn't know where to forward this url to because Apache will only forward a url to Tornado app if it starts with /tornado/.

To fix this, from the Tornado app, redirect using the complete url: /tornado/auth/login.

BTW, you should just prepend the /tornado/ url prefix to every route of your Tornado app, and from Apache just pass the full url to Tornado. This is a simpler setup. Is there a reason you're not doing this?

Update: Here's a diagram to better understand what's going on:

enter image description here