diff --git a/opentelemetry-otlp/CHANGELOG.md b/opentelemetry-otlp/CHANGELOG.md index 14e02560eb..28d519daa2 100644 --- a/opentelemetry-otlp/CHANGELOG.md +++ b/opentelemetry-otlp/CHANGELOG.md @@ -2,6 +2,10 @@ ## vNext +### Fixed + +- URL encoded values in `OTEL_EXPORTER_OTLP_HEADERS` are now correctly decoded. [#1578](https://github.com/open-telemetry/opentelemetry-rust/pull/1578) + ### Added - Added `DeltaTemporalitySelector` ([#1568]) diff --git a/opentelemetry-otlp/src/exporter/http/mod.rs b/opentelemetry-otlp/src/exporter/http/mod.rs index 6cd90203a7..a8b4e3d0c3 100644 --- a/opentelemetry-otlp/src/exporter/http/mod.rs +++ b/opentelemetry-otlp/src/exporter/http/mod.rs @@ -120,7 +120,11 @@ impl HttpExporterBuilder { pub fn with_headers(mut self, headers: HashMap) -> Self { // headers will be wrapped, so we must do some logic to unwrap first. let mut inst_headers = self.http_config.headers.unwrap_or_default(); - inst_headers.extend(headers); + inst_headers.extend( + headers + .into_iter() + .map(|(key, value)| (key, super::url_decode(&value).unwrap_or(value))), + ); self.http_config.headers = Some(inst_headers); self } @@ -310,7 +314,7 @@ fn add_header_from_string(input: &str, headers: &mut HashMap WithExportConfig for B { } #[cfg(any(feature = "grpc-tonic", feature = "http-proto"))] -fn parse_header_string(value: &str) -> impl Iterator { +fn url_decode(value: &str) -> Option { + let mut result = String::with_capacity(value.len()); + let mut chars_to_decode = Vec::::new(); + let mut all_chars = value.chars(); + + loop { + let ch = all_chars.next(); + + if ch.is_some() && ch.unwrap() == '%' { + chars_to_decode.push( + u8::from_str_radix(&format!("{}{}", all_chars.next()?, all_chars.next()?), 16) + .ok()?, + ); + continue; + } + + if !chars_to_decode.is_empty() { + result.push_str(std::str::from_utf8(&chars_to_decode).ok()?); + chars_to_decode.clear(); + } + + if let Some(c) = ch { + result.push(c); + } else { + return Some(result); + } + } +} + +#[cfg(any(feature = "grpc-tonic", feature = "http-proto"))] +fn parse_header_string(value: &str) -> impl Iterator { value .split_terminator(',') .map(str::trim) @@ -219,10 +249,15 @@ fn parse_header_string(value: &str) -> impl Iterator { } #[cfg(any(feature = "grpc-tonic", feature = "http-proto"))] -fn parse_header_key_value_string(key_value_string: &str) -> Option<(&str, &str)> { +fn parse_header_key_value_string(key_value_string: &str) -> Option<(&str, String)> { key_value_string .split_once('=') - .map(|(key, value)| (key.trim(), value.trim())) + .map(|(key, value)| { + ( + key.trim(), + url_decode(value.trim()).unwrap_or(value.to_string()), + ) + }) .filter(|(key, value)| !key.is_empty() && !value.is_empty()) } @@ -267,6 +302,24 @@ mod tests { ); } + #[test] + fn test_url_decode() { + let test_cases = vec![ + // Format: (encoded, expected_decoded) + ("v%201", Some("v 1")), + ("v 1", Some("v 1")), + ("%C3%B6%C3%A0%C2%A7%C3%96abcd%C3%84", Some("öà§ÖabcdÄ")), + ("v%XX1", None), + ]; + + for (encoded, expected_decoded) in test_cases { + assert_eq!( + super::url_decode(encoded), + expected_decoded.map(|v| v.to_string()), + ) + } + } + #[test] fn test_parse_header_string() { let test_cases = vec![ @@ -280,7 +333,10 @@ mod tests { for (input_str, expected_headers) in test_cases { assert_eq!( super::parse_header_string(input_str).collect::>(), - expected_headers, + expected_headers + .into_iter() + .map(|(k, v)| (k, v.to_string())) + .collect::>(), ) } } @@ -290,6 +346,15 @@ mod tests { let test_cases = vec![ // Format: (input_str, expected_header) ("k1=v1", Some(("k1", "v1"))), + ( + "Authentication=Basic AAA", + Some(("Authentication", "Basic AAA")), + ), + ( + "Authentication=Basic%20AAA", + Some(("Authentication", "Basic AAA")), + ), + ("k1=%XX", Some(("k1", "%XX"))), ("", None), ("=v1", None), ("k1=", None), @@ -298,7 +363,7 @@ mod tests { for (input_str, expected_headers) in test_cases { assert_eq!( super::parse_header_key_value_string(input_str), - expected_headers, + expected_headers.map(|(k, v)| (k, v.to_string())), ) } } diff --git a/opentelemetry-otlp/src/exporter/tonic/mod.rs b/opentelemetry-otlp/src/exporter/tonic/mod.rs index 6ef5e6f1f6..61e142e743 100644 --- a/opentelemetry-otlp/src/exporter/tonic/mod.rs +++ b/opentelemetry-otlp/src/exporter/tonic/mod.rs @@ -381,7 +381,7 @@ fn parse_headers_from_env(signal_headers_var: &str) -> HeaderMap { .filter_map(|(key, value)| { Some(( HeaderName::from_str(key).ok()?, - HeaderValue::from_str(value).ok()?, + HeaderValue::from_str(&value).ok()?, )) }) .collect::()