Repository: mlflow/mlflow

Issue:
Unable to run mlflow behind jupyter-server-proxy
### System information
- **Have I written custom code (as opposed to using a stock example script provided in MLflow)**: No
- **OS Platform and Distribution (e.g., Linux Ubuntu 16.04)**: Amazon SageMaker (Amazon Linux)
- **MLflow installed from (source or binary)**: pip install
- **MLflow version (run ``mlflow --version``)**: latest
- **Python version**: 3
- **npm version (if running the dev UI): n/a
- **Exact command to reproduce**: See https://stackoverflow.com/questions/55593128/how-to-run-the-mlflow-web-based-user-interface-from-amazon-sagemaker

### Describe the problem
In a hosted environment such as Amazon SageMaker or JupyterHub, a user might want to run mlflow behind the popular [jupyter-server-proxy](https://github.com/jupyterhub/jupyter-server-proxy) extension.  This allows them to use Jupyter alongside mlflow on the same host.

Unfortunately, the way this works is that mlflow is proxied to a path such as http://myjupyter-server.com/proxy/5000 instead of http://127.0.0.1:5000.  Because there are a lot of references to paths that don't exist, the UI doesn't load.

I was almost able to get mlflow up and running, I get as far as the "Oops! Something went wrong." page, but nothing beyond that.

### Source code / logs

1. Create a SageMaker Notebook Instance.
2. !pip install mlflow
3. !mlflow
4. Open http://mynotebook.notebook.region.sagemaker.aws/proxy/5000/
5. "Oops! Something went wrong." (Niagra falls.)


Discussion:
+1 

I'd like to expose MlFlow from behind an Nginx Server, with an application path `/mlflow` and the hard-coded references to `$HOST:$PORT` disallow this.
+1
+1
+1
+1
+1
The reason why the website fails, is that there are api calls in the javascript code which are executedon the client side. The definition of these api calls are in mlflow/server/js/src/sdk/MlflowService.js - which is automatically generated based on the proto definitions. 

When you load the website on the client side, the api endpoints (/ajax-api/...) in the javascript code are combined with the hostname and the port number, but not the path of the url. 

I haven't run it on sagemaker, and without seeing the logs from the client side, it's hard to tell if it's the same issue. If it is the same issue, you can change the api path in the source code of mlflow and then compile a custom mlflow package. In regards to the port number, maybe you could run mlflow server on port 80. 

If you're running mlflow behind an nginx reverse proxy you can a filter to update the path in the javascript code that is sent to the client when serving the files: 

````
location /mlflow/ {
    proxy_pass http://localhost:5000/;
    sub_filter '/ajax-api' '/mlflow/ajax-api';
    sub_filter_once off;
    sub_filter_types *;
}
````




It should be fixed by removing the leading `/` from the JavaScript calls. To fix this, I'm sadly missing documentation on how `mlflow/server/js/src/sdk/MlflowService.js` is generated. This is not covered by `generate-protos.sh`.

Resolving patch:
diff --git a/mlflow/server/__init__.py b/mlflow/server/__init__.py
--- a/mlflow/server/__init__.py
+++ b/mlflow/server/__init__.py
@@ -4,31 +4,24 @@
 from flask import Flask, send_from_directory
 
 from mlflow.server import handlers
-from mlflow.server.handlers import get_artifact_handler
+from mlflow.server.handlers import get_artifact_handler, STATIC_PREFIX_ENV_VAR, _add_static_prefix
 from mlflow.utils.process import exec_cmd
 
 # NB: These are intenrnal environment variables used for communication between
 # the cli and the forked gunicorn processes.
 BACKEND_STORE_URI_ENV_VAR = "_MLFLOW_SERVER_FILE_STORE"
 ARTIFACT_ROOT_ENV_VAR = "_MLFLOW_SERVER_ARTIFACT_ROOT"
-STATIC_PREFIX_ENV_VAR = "_MLFLOW_STATIC_PREFIX"
 
 REL_STATIC_DIR = "js/build"
 
 app = Flask(__name__, static_folder=REL_STATIC_DIR)
 STATIC_DIR = os.path.join(app.root_path, REL_STATIC_DIR)
 
+
 for http_path, handler, methods in handlers.get_endpoints():
     app.add_url_rule(http_path, handler.__name__, handler, methods=methods)
 
 
-def _add_static_prefix(route):
-    prefix = os.environ.get(STATIC_PREFIX_ENV_VAR)
-    if prefix:
-        return prefix + route
-    return route
-
-
 # Serve the "get-artifact" route.
 @app.route(_add_static_prefix('/get-artifact'))
 def serve_artifacts():
diff --git a/mlflow/server/handlers.py b/mlflow/server/handlers.py
--- a/mlflow/server/handlers.py
+++ b/mlflow/server/handlers.py
@@ -22,6 +22,14 @@
 from mlflow.utils.validation import _validate_batch_log_api_req
 
 _store = None
+STATIC_PREFIX_ENV_VAR = "_MLFLOW_STATIC_PREFIX"
+
+
+def _add_static_prefix(route):
+    prefix = os.environ.get(STATIC_PREFIX_ENV_VAR)
+    if prefix:
+        return prefix + route
+    return route
 
 
 def _get_store(backend_store_uri=None, default_artifact_root=None):
@@ -360,7 +368,7 @@ def _get_paths(base_path):
     We should register paths like /api/2.0/preview/mlflow/experiment and
     /ajax-api/2.0/preview/mlflow/experiment in the Flask router.
     """
-    return ['/api/2.0{}'.format(base_path), '/ajax-api/2.0{}'.format(base_path)]
+    return ['/api/2.0{}'.format(base_path), _add_static_prefix('/ajax-api/2.0{}'.format(base_path))]
 
 
 def get_endpoints():


Test patch: