Repository: mlflow/mlflow

Issue:
[BUG] Ai gateway error on search_route when no route exists
### Issues Policy acknowledgement

- [X] I have read and agree to submit bug reports in accordance with the [issues policy](https://www.github.com/mlflow/mlflow/blob/master/ISSUE_POLICY.md)

### Willingness to contribute

Yes. I can contribute a fix for this bug independently.

### MLflow version

- Client: 2.6.0


### System information

- **OS Platform and Distribution (e.g., Linux Ubuntu 16.04)**:
- **Python version**:
- **yarn version, if running the dev UI**:


### Describe the problem

search_routes throws an error when no route exist. Instead it should return an empty list.

```shell
File /local_disk0/.ephemeral_nfs/envs/pythonEnv-be19adf4-4d5d-48a3-96dd-98fdc4bc40ef/lib/python3.10/site-packages/mlflow/gateway/client.py:125, in MlflowGatewayClient.search_routes(self, page_token)
    121 request_parameters = {"page_token": page_token} if page_token is not None else None
    122 response_json = self._call_endpoint(
    123     "GET", MLFLOW_GATEWAY_CRUD_ROUTE_BASE, json_body=json.dumps(request_parameters)
    124 ).json()
--> 125 for route in response_json["routes"]:
    126     route["route_url"] = resolve_route_url(self._gateway_uri, route["route_url"])
    128 routes = [Route(**resp) for resp in response_json["routes"]]
```

### Tracking information

_No response_

### Code to reproduce issue

```
from mlflow import gateway
gateway.set_gateway_uri(gateway_uri="databricks")
routes = gateway.search_routes()

```

### Stack trace

<!-- PLEASE KEEP BACKTICKS AND CHECK PREVIEW -->
```shell

--------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
File <command-1135807300123198>:10
      7 #define our embedding route name, this is the endpoint we'll call for our embeddings
      8 embedding_route_name = 'dbdemos_embeddings_route'
---> 10 routes = gateway.search_routes()
     11 route_exists = any(route for route in routes if route.name == embedding_route_name)
     13 if route_exists:

File /local_disk0/.ephemeral_nfs/envs/pythonEnv-be19adf4-4d5d-48a3-96dd-98fdc4bc40ef/lib/python3.10/site-packages/mlflow/gateway/fluent.py:41, in search_routes()
     38 def pagination_wrapper_func(_, next_page_token):
     39     return MlflowGatewayClient().search_routes(page_token=next_page_token)
---> 41 return get_results_from_paginated_fn(
     42     paginated_fn=pagination_wrapper_func,
     43     max_results_per_page=MLFLOW_GATEWAY_SEARCH_ROUTES_PAGE_SIZE,
     44     max_results=None,
     45 )

File /local_disk0/.ephemeral_nfs/envs/pythonEnv-be19adf4-4d5d-48a3-96dd-98fdc4bc40ef/lib/python3.10/site-packages/mlflow/utils/__init__.py:264, in get_results_from_paginated_fn(paginated_fn, max_results_per_page, max_results)
    262     page_results = paginated_fn(num_to_get, next_page_token)
    263 else:
--> 264     page_results = paginated_fn(max_results_per_page, next_page_token)
    265 all_results.extend(page_results)
    266 if hasattr(page_results, "token") and page_results.token:

File /local_disk0/.ephemeral_nfs/envs/pythonEnv-be19adf4-4d5d-48a3-96dd-98fdc4bc40ef/lib/python3.10/site-packages/mlflow/gateway/fluent.py:39, in search_routes.<locals>.pagination_wrapper_func(_, next_page_token)
     38 def pagination_wrapper_func(_, next_page_token):
---> 39     return MlflowGatewayClient().search_routes(page_token=next_page_token)

File /local_disk0/.ephemeral_nfs/envs/pythonEnv-be19adf4-4d5d-48a3-96dd-98fdc4bc40ef/lib/python3.10/site-packages/mlflow/gateway/client.py:125, in MlflowGatewayClient.search_routes(self, page_token)
    121 request_parameters = {"page_token": page_token} if page_token is not None else None
    122 response_json = self._call_endpoint(
    123     "GET", MLFLOW_GATEWAY_CRUD_ROUTE_BASE, json_body=json.dumps(request_parameters)
    124 ).json()
--> 125 for route in response_json["routes"]:
    126     route["route_url"] = resolve_route_url(self._gateway_uri, route["route_url"])
    128 routes = [Route(**resp) for resp in response_json["routes"]]


```


### Other info / logs

<!-- PLEASE KEEP BACKTICKS AND CHECK PREVIEW -->
```
REPLACE_ME
```


### What component(s) does this bug affect?

- [ ] `area/artifacts`: Artifact stores and artifact logging
- [ ] `area/build`: Build and test infrastructure for MLflow
- [ ] `area/docs`: MLflow documentation pages
- [ ] `area/examples`: Example code
- [X] `area/gateway`: AI Gateway service, Gateway client APIs, third-party Gateway integrations
- [ ] `area/model-registry`: Model Registry service, APIs, and the fluent client calls for Model Registry
- [ ] `area/models`: MLmodel format, model serialization/deserialization, flavors
- [ ] `area/recipes`: Recipes, Recipe APIs, Recipe configs, Recipe Templates
- [ ] `area/projects`: MLproject format, project running backends
- [ ] `area/scoring`: MLflow Model server, model deployment tools, Spark UDFs
- [ ] `area/server-infra`: MLflow Tracking server backend
- [ ] `area/tracking`: Tracking Service, tracking client APIs, autologging

### What interface(s) does this bug affect?

- [ ] `area/uiux`: Front-end, user experience, plotting, JavaScript, JavaScript dev server
- [ ] `area/docker`: Docker use across MLflow's components, such as MLflow Projects and MLflow Models
- [ ] `area/sqlalchemy`: Use of SQLAlchemy in the Tracking Service or Model Registry
- [ ] `area/windows`: Windows support

### What language(s) does this bug affect?

- [ ] `language/r`: R APIs and clients
- [ ] `language/java`: Java APIs and clients
- [ ] `language/new`: Proposals for new client languages

### What integration(s) does this bug affect?

- [ ] `integrations/azure`: Azure and Azure ML integrations
- [ ] `integrations/sagemaker`: SageMaker integrations
- [ ] `integrations/databricks`: Databricks integrations


Discussion:

Resolving patch:
diff --git a/mlflow/gateway/client.py b/mlflow/gateway/client.py
--- a/mlflow/gateway/client.py
+++ b/mlflow/gateway/client.py
@@ -122,10 +122,18 @@ def search_routes(self, page_token: Optional[str] = None) -> PagedList[Route]:
         response_json = self._call_endpoint(
             "GET", MLFLOW_GATEWAY_CRUD_ROUTE_BASE, json_body=json.dumps(request_parameters)
         ).json()
-        for route in response_json["routes"]:
-            route["route_url"] = resolve_route_url(self._gateway_uri, route["route_url"])
-
-        routes = [Route(**resp) for resp in response_json["routes"]]
+        routes = [
+            Route(
+                **{
+                    **resp,
+                    "route_url": resolve_route_url(
+                        self._gateway_uri,
+                        resp["route_url"],
+                    ),
+                }
+            )
+            for resp in response_json.get("routes", [])
+        ]
         next_page_token = response_json.get("next_page_token")
         return PagedList(routes, next_page_token)
 


Test patch:
diff --git a/tests/gateway/test_client.py b/tests/gateway/test_client.py
--- a/tests/gateway/test_client.py
+++ b/tests/gateway/test_client.py
@@ -645,3 +645,14 @@ def test_client_query_mlflow_embeddings_route(oss_gateway):
     with mock.patch.object(gateway_client, "_call_endpoint", return_value=mock_response):
         response = gateway_client.query(route="embeddings-oss", data=data)
         assert response == expected_output
+
+
+def test_search_routes_no_routes():
+    gateway_client = MlflowGatewayClient(gateway_uri="http://localhost:5000")
+    resp = mock.Mock(status_code=200)
+    resp.json.return_value = {}
+    with mock.patch("requests.Session.request", return_value=resp) as request_mock:
+        routes = gateway_client.search_routes()
+        request_mock.assert_called_once()
+        assert routes == []
+        assert routes.token is None