Repository: mlflow/mlflow

Issue:
[BUG] (new in 1.26) logging param with "" (empty string) as value yields error
### Willingness to contribute

Yes. I would be willing to contribute a fix for this bug with guidance from the MLflow community.

### MLflow version

1.26.x

### System information

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


### Describe the problem

After upgrading mlflow server to version >=1.26, a client-side call to mlflow.log_param() with an empty string as value fails.

This is an issue because:
- breaks previous behaviour
- empty string is a valid string (and according to documentation, value is of type Any and will be stringified)
- it's a valid use case for some parameters to be empty for some runs



### Tracking information

```bash
mlflow server -p 6000  
```

```python
>>> print("MLflow version:", mlflow.__version__)
MLflow version: 1.26.0
>>> print("Tracking URI:", mlflow.get_tracking_uri())
Tracking URI: http://127.0.0.1:6000
>>> print("Artifact URI:", mlflow.get_artifact_uri())
Artifact URI: ./mlruns/0/0a4ca1ed15854f9a8b01acff703c406a/artifacts

### Code to reproduce issue

```python
import os, mlflow
os.environ['MLFLOW_TRACKING_URI']="http://127.0.0.1:6000"

with mlflow.start_run():
   mlflow.log_param('param_with_empty_string_value', '')
```



### Other info / logs

```python-traceback

Traceback (most recent call last):
  File "/venv/lib/python3.8/site-packages/mlflow/tracking/_tracking_service/client.py", line 234, in log_param
    self.store.log_param(run_id, param)
  File "/venv/lib/python3.8/site-packages/mlflow/store/tracking/rest_store.py", line 203, in log_param
    self._call_endpoint(LogParam, req_body)
  File "/venv/lib/python3.8/site-packages/mlflow/store/tracking/rest_store.py", line 56, in _call_endpoint
    return call_endpoint(self.get_host_creds(), endpoint, method, json_body, response_proto)
  File "/venv/lib/python3.8/site-packages/mlflow/utils/rest_utils.py", line 256, in call_endpoint
    response = verify_rest_response(response, endpoint)
  File "/venv/lib/python3.8/site-packages/mlflow/utils/rest_utils.py", line 185, in verify_rest_response
    raise RestException(json.loads(response.text))
mlflow.exceptions.RestException: INVALID_PARAMETER_VALUE: Missing value for required parameter 'value'. See the API docs for more information about request parameters.

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "/venv/lib/python3.8/site-packages/mlflow/tracking/fluent.py", line 505, in log_param
    MlflowClient().log_param(run_id, key, value)
  File "/venv/lib/python3.8/site-packages/mlflow/tracking/client.py", line 743, in log_param
    self._tracking_client.log_param(run_id, key, value)
  File "/venv/lib/python3.8/site-packages/mlflow/tracking/_tracking_service/client.py", line 238, in log_param
    raise MlflowException(msg, INVALID_PARAMETER_VALUE)
mlflow.exceptions.MlflowException: INVALID_PARAMETER_VALUE: Missing value for required parameter 'value'. See the API docs for more information about request parameters.

The cause of this error is typically due to repeated calls
to an individual run_id event logging.

Incorrect Example:
---------------------------------------
with mlflow.start_run():
    mlflow.log_param("depth", 3)
    mlflow.log_param("depth", 5)
---------------------------------------

Which will throw an MlflowException for overwriting a
logged parameter.

Correct Example:
---------------------------------------
with mlflow.start_run():
    with mlflow.start_run(nested=True):
        mlflow.log_param("depth", 3)
    with mlflow.start_run(nested=True):
        mlflow.log_param("depth", 5)
---------------------------------------

Which will create a new nested run for each individual
model and prevent parameter key collisions within the
tracking store.'

```



### 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
- [ ] `area/model-registry`: Model Registry service, APIs, and the fluent client calls for Model Registry
- [ ] `area/models`: MLmodel format, model serialization/deserialization, flavors
- [ ] `area/projects`: MLproject format, project running backends
- [ ] `area/scoring`: MLflow Model server, model deployment tools, Spark UDFs
- [ ] `area/server-infra`: MLflow Tracking server backend
- [X] `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:
@matthiasjfrank Thanks for reporting this issue. I was able to reproduce it. The root cause is this line:

https://github.com/mlflow/mlflow/blob/398345c435421ee2f4ab8d8b3237bc845f25a2c6/mlflow/server/handlers.py#L745

`_assert_required` throws if the given value is an empty string. I think we need to remove it. Would you be interested in creating a PR?



Resolving patch:
diff --git a/mlflow/server/handlers.py b/mlflow/server/handlers.py
--- a/mlflow/server/handlers.py
+++ b/mlflow/server/handlers.py
@@ -742,7 +742,7 @@ def _log_param():
         schema={
             "run_id": [_assert_required, _assert_string],
             "key": [_assert_required, _assert_string],
-            "value": [_assert_required, _assert_string],
+            "value": [_assert_string],
         },
     )
     param = Param(request_message.key, request_message.value)


Test patch:
diff --git a/tests/tracking/test_rest_tracking.py b/tests/tracking/test_rest_tracking.py
--- a/tests/tracking/test_rest_tracking.py
+++ b/tests/tracking/test_rest_tracking.py
@@ -452,14 +452,25 @@ def assert_bad_request(payload, expected_error_message):
         },
         "Invalid value 31 for parameter 'key' supplied",
     )
-    assert_bad_request(
+
+
+def test_log_param_with_empty_string_as_value(mlflow_client, tracking_server_uri):
+    experiment_id = mlflow_client.create_experiment(
+        test_log_param_with_empty_string_as_value.__name__
+    )
+    created_run = mlflow_client.create_run(experiment_id)
+    run_id = created_run.info.run_id
+
+    response = _send_rest_tracking_post_request(
+        tracking_server_uri,
+        "/api/2.0/mlflow/runs/log-parameter",
         {
             "run_id": run_id,
             "key": "param",
-            # Missing value
+            "value": "",
         },
-        "Missing value for required parameter 'value'",
     )
+    assert response.status_code == 200
 
 
 def test_set_tag_validation(mlflow_client, tracking_server_uri):