Skip to content

Commit 58ec379

Browse files
pyohanneslalitbcijothomas
authored
Support URL-encoded values for OTEL_EXPORTER_OTLP_HEADERS (#1578)
Co-authored-by: Lalit Kumar Bhasin <lalit_fin@yahoo.com> Co-authored-by: Cijo Thomas <cijo.thomas@gmail.com>
1 parent 7bdbc47 commit 58ec379

File tree

4 files changed

+81
-8
lines changed

4 files changed

+81
-8
lines changed

opentelemetry-otlp/CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## vNext
44

5+
### Fixed
6+
7+
- URL encoded values in `OTEL_EXPORTER_OTLP_HEADERS` are now correctly decoded. [#1578](https://github.com/open-telemetry/opentelemetry-rust/pull/1578)
8+
59
### Added
610

711
- Added `DeltaTemporalitySelector` ([#1568])

opentelemetry-otlp/src/exporter/http/mod.rs

+6-2
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,11 @@ impl HttpExporterBuilder {
120120
pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
121121
// headers will be wrapped, so we must do some logic to unwrap first.
122122
let mut inst_headers = self.http_config.headers.unwrap_or_default();
123-
inst_headers.extend(headers);
123+
inst_headers.extend(
124+
headers
125+
.into_iter()
126+
.map(|(key, value)| (key, super::url_decode(&value).unwrap_or(value))),
127+
);
124128
self.http_config.headers = Some(inst_headers);
125129
self
126130
}
@@ -310,7 +314,7 @@ fn add_header_from_string(input: &str, headers: &mut HashMap<HeaderName, HeaderV
310314
headers.extend(parse_header_string(input).filter_map(|(key, value)| {
311315
Some((
312316
HeaderName::from_str(key).ok()?,
313-
HeaderValue::from_str(value).ok()?,
317+
HeaderValue::from_str(&value).ok()?,
314318
))
315319
}));
316320
}

opentelemetry-otlp/src/exporter/mod.rs

+70-5
Original file line numberDiff line numberDiff line change
@@ -211,18 +211,53 @@ impl<B: HasExportConfig> WithExportConfig for B {
211211
}
212212

213213
#[cfg(any(feature = "grpc-tonic", feature = "http-proto"))]
214-
fn parse_header_string(value: &str) -> impl Iterator<Item = (&str, &str)> {
214+
fn url_decode(value: &str) -> Option<String> {
215+
let mut result = String::with_capacity(value.len());
216+
let mut chars_to_decode = Vec::<u8>::new();
217+
let mut all_chars = value.chars();
218+
219+
loop {
220+
let ch = all_chars.next();
221+
222+
if ch.is_some() && ch.unwrap() == '%' {
223+
chars_to_decode.push(
224+
u8::from_str_radix(&format!("{}{}", all_chars.next()?, all_chars.next()?), 16)
225+
.ok()?,
226+
);
227+
continue;
228+
}
229+
230+
if !chars_to_decode.is_empty() {
231+
result.push_str(std::str::from_utf8(&chars_to_decode).ok()?);
232+
chars_to_decode.clear();
233+
}
234+
235+
if let Some(c) = ch {
236+
result.push(c);
237+
} else {
238+
return Some(result);
239+
}
240+
}
241+
}
242+
243+
#[cfg(any(feature = "grpc-tonic", feature = "http-proto"))]
244+
fn parse_header_string(value: &str) -> impl Iterator<Item = (&str, String)> {
215245
value
216246
.split_terminator(',')
217247
.map(str::trim)
218248
.filter_map(parse_header_key_value_string)
219249
}
220250

221251
#[cfg(any(feature = "grpc-tonic", feature = "http-proto"))]
222-
fn parse_header_key_value_string(key_value_string: &str) -> Option<(&str, &str)> {
252+
fn parse_header_key_value_string(key_value_string: &str) -> Option<(&str, String)> {
223253
key_value_string
224254
.split_once('=')
225-
.map(|(key, value)| (key.trim(), value.trim()))
255+
.map(|(key, value)| {
256+
(
257+
key.trim(),
258+
url_decode(value.trim()).unwrap_or(value.to_string()),
259+
)
260+
})
226261
.filter(|(key, value)| !key.is_empty() && !value.is_empty())
227262
}
228263

@@ -267,6 +302,24 @@ mod tests {
267302
);
268303
}
269304

305+
#[test]
306+
fn test_url_decode() {
307+
let test_cases = vec![
308+
// Format: (encoded, expected_decoded)
309+
("v%201", Some("v 1")),
310+
("v 1", Some("v 1")),
311+
("%C3%B6%C3%A0%C2%A7%C3%96abcd%C3%84", Some("öà§ÖabcdÄ")),
312+
("v%XX1", None),
313+
];
314+
315+
for (encoded, expected_decoded) in test_cases {
316+
assert_eq!(
317+
super::url_decode(encoded),
318+
expected_decoded.map(|v| v.to_string()),
319+
)
320+
}
321+
}
322+
270323
#[test]
271324
fn test_parse_header_string() {
272325
let test_cases = vec![
@@ -280,7 +333,10 @@ mod tests {
280333
for (input_str, expected_headers) in test_cases {
281334
assert_eq!(
282335
super::parse_header_string(input_str).collect::<Vec<_>>(),
283-
expected_headers,
336+
expected_headers
337+
.into_iter()
338+
.map(|(k, v)| (k, v.to_string()))
339+
.collect::<Vec<_>>(),
284340
)
285341
}
286342
}
@@ -290,6 +346,15 @@ mod tests {
290346
let test_cases = vec![
291347
// Format: (input_str, expected_header)
292348
("k1=v1", Some(("k1", "v1"))),
349+
(
350+
"Authentication=Basic AAA",
351+
Some(("Authentication", "Basic AAA")),
352+
),
353+
(
354+
"Authentication=Basic%20AAA",
355+
Some(("Authentication", "Basic AAA")),
356+
),
357+
("k1=%XX", Some(("k1", "%XX"))),
293358
("", None),
294359
("=v1", None),
295360
("k1=", None),
@@ -298,7 +363,7 @@ mod tests {
298363
for (input_str, expected_headers) in test_cases {
299364
assert_eq!(
300365
super::parse_header_key_value_string(input_str),
301-
expected_headers,
366+
expected_headers.map(|(k, v)| (k, v.to_string())),
302367
)
303368
}
304369
}

opentelemetry-otlp/src/exporter/tonic/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ fn parse_headers_from_env(signal_headers_var: &str) -> HeaderMap {
381381
.filter_map(|(key, value)| {
382382
Some((
383383
HeaderName::from_str(key).ok()?,
384-
HeaderValue::from_str(value).ok()?,
384+
HeaderValue::from_str(&value).ok()?,
385385
))
386386
})
387387
.collect::<HeaderMap>()

0 commit comments

Comments
 (0)