Repository: open-telemetry/opentelemetry-python

Issue:
format_baggage does not escape non-ascii in baggage keys
https://github.com/open-telemetry/opentelemetry-python/blob/4250078e43ddb24c88e19270c7af01ae63336fb9/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py#L100

The cpp implementation does this, and it looks like in python test strings there is url encoding.
https://github.com/open-telemetry/opentelemetry-cpp/blob/61d3c5e318830d10a0859befa046aa4847593764/api/include/opentelemetry/baggage/baggage.h#L174


Discussion:

Resolving patch:
diff --git a/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py
--- a/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py
+++ b/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py
@@ -13,9 +13,9 @@
 # limitations under the License.
 #
 import typing
-import urllib.parse
+from urllib.parse import quote_plus, unquote_plus
 
-from opentelemetry import baggage
+from opentelemetry.baggage import get_all, set_baggage
 from opentelemetry.context import get_current
 from opentelemetry.context.context import Context
 from opentelemetry.propagators import textmap
@@ -63,9 +63,9 @@ def extract(
                 name, value = entry.split("=", 1)
             except Exception:  # pylint: disable=broad-except
                 continue
-            context = baggage.set_baggage(
-                urllib.parse.unquote(name).strip(),
-                urllib.parse.unquote(value).strip(),
+            context = set_baggage(
+                unquote_plus(name).strip(),
+                unquote_plus(value).strip(),
                 context=context,
             )
 
@@ -82,7 +82,7 @@ def inject(
         See
         `opentelemetry.propagators.textmap.TextMapPropagator.inject`
         """
-        baggage_entries = baggage.get_all(context=context)
+        baggage_entries = get_all(context=context)
         if not baggage_entries:
             return
 
@@ -97,7 +97,7 @@ def fields(self) -> typing.Set[str]:
 
 def _format_baggage(baggage_entries: typing.Mapping[str, object]) -> str:
     return ",".join(
-        key + "=" + urllib.parse.quote_plus(str(value))
+        quote_plus(str(key)) + "=" + quote_plus(str(value))
         for key, value in baggage_entries.items()
     )
 


Test patch:
diff --git a/opentelemetry-api/tests/baggage/test_baggage_propagation.py b/opentelemetry-api/tests/baggage/test_baggage_propagation.py
--- a/opentelemetry-api/tests/baggage/test_baggage_propagation.py
+++ b/opentelemetry-api/tests/baggage/test_baggage_propagation.py
@@ -18,7 +18,10 @@
 from unittest.mock import Mock, patch
 
 from opentelemetry import baggage
-from opentelemetry.baggage.propagation import W3CBaggagePropagator
+from opentelemetry.baggage.propagation import (
+    W3CBaggagePropagator,
+    _format_baggage,
+)
 from opentelemetry.context import get_current
 
 
@@ -106,6 +109,15 @@ def test_header_contains_pair_too_long(self):
         expected = {"key1": "value1", "key3": "value3"}
         self.assertEqual(self._extract(header), expected)
 
+    def test_extract_unquote_plus(self):
+        self.assertEqual(
+            self._extract("key+key=value+value"), {"key key": "value value"}
+        )
+        self.assertEqual(
+            self._extract("key%2Fkey=value%2Fvalue"),
+            {"key/key": "value/value"},
+        )
+
     def test_inject_no_baggage_entries(self):
         values = {}
         output = self._inject(values)
@@ -140,7 +152,7 @@ def test_inject_non_string_values(self):
         self.assertIn("key2=123", output)
         self.assertIn("key3=123.567", output)
 
-    @patch("opentelemetry.baggage.propagation.baggage")
+    @patch("opentelemetry.baggage.propagation.get_all")
     @patch("opentelemetry.baggage.propagation._format_baggage")
     def test_fields(self, mock_format_baggage, mock_baggage):
 
@@ -154,3 +166,12 @@ def test_fields(self, mock_format_baggage, mock_baggage):
             inject_fields.add(mock_call[1][1])
 
         self.assertEqual(inject_fields, self.propagator.fields)
+
+    def test__format_baggage(self):
+        self.assertEqual(
+            _format_baggage({"key key": "value value"}), "key+key=value+value"
+        )
+        self.assertEqual(
+            _format_baggage({"key/key": "value/value"}),
+            "key%2Fkey=value%2Fvalue",
+        )