Skip to content

Commit 042372d

Browse files
authored
Add support for configuring client-wide gRPC binary metadata values, validate metadata earlier (#997)
1 parent 3a6e5ff commit 042372d

File tree

3 files changed

+216
-22
lines changed

3 files changed

+216
-22
lines changed

client/src/lib.rs

Lines changed: 214 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,10 @@ use tonic::{
7878
body::Body,
7979
client::GrpcService,
8080
codegen::InterceptedService,
81-
metadata::{MetadataKey, MetadataMap, MetadataValue},
81+
metadata::{
82+
AsciiMetadataKey, AsciiMetadataValue, BinaryMetadataKey, BinaryMetadataValue, MetadataMap,
83+
MetadataValue,
84+
},
8285
service::Interceptor,
8386
transport::{Certificate, Channel, Endpoint, Identity},
8487
};
@@ -146,9 +149,20 @@ pub struct ClientOptions {
146149
pub keep_alive: Option<ClientKeepAliveConfig>,
147150

148151
/// HTTP headers to include on every RPC call.
152+
///
153+
/// These must be valid gRPC metadata keys, and must not be binary metadata keys (ending in
154+
/// `-bin). To set binary headers, use [ClientOptions::binary_headers]. Invalid header keys or
155+
/// values will cause an error to be returned when connecting.
149156
#[builder(default)]
150157
pub headers: Option<HashMap<String, String>>,
151158

159+
/// HTTP headers to include on every RPC call as binary gRPC metadata (encoded as base64).
160+
///
161+
/// These must be valid binary gRPC metadata keys (and end with a `-bin` suffix). Invalid
162+
/// header keys will cause an error to be returned when connecting.
163+
#[builder(default)]
164+
pub binary_headers: Option<HashMap<String, Vec<u8>>>,
165+
152166
/// API key which is set as the "Authorization" header with "Bearer " prepended. This will only
153167
/// be applied if the headers don't already have an "Authorization" header.
154168
#[builder(default)]
@@ -322,6 +336,9 @@ pub enum ClientInitError {
322336
/// Invalid URI. Configuration error, fatal.
323337
#[error("Invalid URI: {0:?}")]
324338
InvalidUri(#[from] InvalidUri),
339+
/// Invalid gRPC metadata headers. Configuration error.
340+
#[error("Invalid headers: {0}")]
341+
InvalidHeaders(#[from] InvalidHeaderError),
325342
/// Server connection error. Crashing and restarting the worker is likely best.
326343
#[error("Server connection error: {0:?}")]
327344
TonicTransportError(#[from] tonic::transport::Error),
@@ -331,6 +348,37 @@ pub enum ClientInitError {
331348
SystemInfoCallError(tonic::Status),
332349
}
333350

351+
/// Errors thrown when a gRPC metadata header is invalid.
352+
#[derive(thiserror::Error, Debug)]
353+
pub enum InvalidHeaderError {
354+
/// A binary header key was invalid
355+
#[error("Invalid binary header key '{key}': {source}")]
356+
InvalidBinaryHeaderKey {
357+
/// The invalid key
358+
key: String,
359+
/// The source error from tonic
360+
source: tonic::metadata::errors::InvalidMetadataKey,
361+
},
362+
/// An ASCII header key was invalid
363+
#[error("Invalid ASCII header key '{key}': {source}")]
364+
InvalidAsciiHeaderKey {
365+
/// The invalid key
366+
key: String,
367+
/// The source error from tonic
368+
source: tonic::metadata::errors::InvalidMetadataKey,
369+
},
370+
/// An ASCII header value was invalid
371+
#[error("Invalid ASCII header value for key '{key}': {source}")]
372+
InvalidAsciiHeaderValue {
373+
/// The key
374+
key: String,
375+
/// The invalid value
376+
value: String,
377+
/// The source error from tonic
378+
source: tonic::metadata::errors::InvalidMetadataValue,
379+
},
380+
}
381+
334382
/// A client with [ClientOptions] attached, which can be passed to initialize workers,
335383
/// or can be used directly. Is cheap to clone.
336384
#[derive(Clone, Debug)]
@@ -344,9 +392,33 @@ pub struct ConfiguredClient<C> {
344392
}
345393

346394
impl<C> ConfiguredClient<C> {
347-
/// Set HTTP request headers overwriting previous headers
348-
pub fn set_headers(&self, headers: HashMap<String, String>) {
349-
self.headers.write().user_headers = headers;
395+
/// Set HTTP request headers overwriting previous headers.
396+
///
397+
/// This will not affect headers set via [ClientOptions::binary_headers].
398+
///
399+
/// # Errors
400+
///
401+
/// Will return an error if any of the provided keys or values are not valid gRPC metadata.
402+
/// If an error is returned, the previous headers will remain unchanged.
403+
pub fn set_headers(&self, headers: HashMap<String, String>) -> Result<(), InvalidHeaderError> {
404+
self.headers.write().user_headers = parse_ascii_headers(headers)?;
405+
Ok(())
406+
}
407+
408+
/// Set binary HTTP request headers overwriting previous headers.
409+
///
410+
/// This will not affect headers set via [ClientOptions::headers].
411+
///
412+
/// # Errors
413+
///
414+
/// Will return an error if any of the provided keys are not valid gRPC binary metadata keys.
415+
/// If an error is returned, the previous headers will remain unchanged.
416+
pub fn set_binary_headers(
417+
&self,
418+
binary_headers: HashMap<String, Vec<u8>>,
419+
) -> Result<(), InvalidHeaderError> {
420+
self.headers.write().user_binary_headers = parse_binary_headers(binary_headers)?;
421+
Ok(())
350422
}
351423

352424
/// Set API key, overwriting previous
@@ -373,7 +445,8 @@ impl<C> ConfiguredClient<C> {
373445

374446
#[derive(Debug)]
375447
struct ClientHeaders {
376-
user_headers: HashMap<String, String>,
448+
user_headers: HashMap<AsciiMetadataKey, AsciiMetadataValue>,
449+
user_binary_headers: HashMap<BinaryMetadataKey, BinaryMetadataValue>,
377450
api_key: Option<String>,
378451
}
379452

@@ -382,10 +455,13 @@ impl ClientHeaders {
382455
for (key, val) in self.user_headers.iter() {
383456
// Only if not already present
384457
if !metadata.contains_key(key) {
385-
// Ignore invalid keys/values
386-
if let (Ok(key), Ok(val)) = (MetadataKey::from_str(key), val.parse()) {
387-
metadata.insert(key, val);
388-
}
458+
metadata.insert(key, val.clone());
459+
}
460+
}
461+
for (key, val) in self.user_binary_headers.iter() {
462+
// Only if not already present
463+
if !metadata.contains_key(key) {
464+
metadata.insert_bin(key, val.clone());
389465
}
390466
}
391467
if let Some(api_key) = &self.api_key {
@@ -491,7 +567,10 @@ impl ClientOptions {
491567
};
492568

493569
let headers = Arc::new(RwLock::new(ClientHeaders {
494-
user_headers: self.headers.clone().unwrap_or_default(),
570+
user_headers: parse_ascii_headers(self.headers.clone().unwrap_or_default())?,
571+
user_binary_headers: parse_binary_headers(
572+
self.binary_headers.clone().unwrap_or_default(),
573+
)?,
495574
api_key: self.api_key.clone(),
496575
}));
497576
let interceptor = ServiceCallInterceptor {
@@ -558,6 +637,57 @@ impl ClientOptions {
558637
}
559638
}
560639

640+
fn parse_ascii_headers(
641+
headers: HashMap<String, String>,
642+
) -> Result<HashMap<AsciiMetadataKey, AsciiMetadataValue>, InvalidHeaderError> {
643+
let mut parsed_headers = HashMap::with_capacity(headers.len());
644+
for (k, v) in headers.into_iter() {
645+
let key = match AsciiMetadataKey::from_str(&k) {
646+
Ok(key) => key,
647+
Err(err) => {
648+
return Err(InvalidHeaderError::InvalidAsciiHeaderKey {
649+
key: k,
650+
source: err,
651+
});
652+
}
653+
};
654+
let value = match MetadataValue::from_str(&v) {
655+
Ok(value) => value,
656+
Err(err) => {
657+
return Err(InvalidHeaderError::InvalidAsciiHeaderValue {
658+
key: k,
659+
value: v,
660+
source: err,
661+
});
662+
}
663+
};
664+
parsed_headers.insert(key, value);
665+
}
666+
667+
Ok(parsed_headers)
668+
}
669+
670+
fn parse_binary_headers(
671+
headers: HashMap<String, Vec<u8>>,
672+
) -> Result<HashMap<BinaryMetadataKey, BinaryMetadataValue>, InvalidHeaderError> {
673+
let mut parsed_headers = HashMap::with_capacity(headers.len());
674+
for (k, v) in headers.into_iter() {
675+
let key = match BinaryMetadataKey::from_str(&k) {
676+
Ok(key) => key,
677+
Err(err) => {
678+
return Err(InvalidHeaderError::InvalidBinaryHeaderKey {
679+
key: k,
680+
source: err,
681+
});
682+
}
683+
};
684+
let value = BinaryMetadataValue::from_bytes(&v);
685+
parsed_headers.insert(key, value);
686+
}
687+
688+
Ok(parsed_headers)
689+
}
690+
561691
/// Interceptor which attaches common metadata (like "client-name") to every outgoing call
562692
#[derive(Clone)]
563693
pub struct ServiceCallInterceptor {
@@ -1770,13 +1900,17 @@ mod tests {
17701900
// Initial header set
17711901
let headers = Arc::new(RwLock::new(ClientHeaders {
17721902
user_headers: HashMap::new(),
1903+
user_binary_headers: HashMap::new(),
17731904
api_key: Some("my-api-key".to_owned()),
17741905
}));
1775-
headers
1776-
.clone()
1777-
.write()
1778-
.user_headers
1779-
.insert("my-meta-key".to_owned(), "my-meta-val".to_owned());
1906+
headers.clone().write().user_headers.insert(
1907+
"my-meta-key".parse().unwrap(),
1908+
"my-meta-val".parse().unwrap(),
1909+
);
1910+
headers.clone().write().user_binary_headers.insert(
1911+
"my-bin-meta-key-bin".parse().unwrap(),
1912+
vec![1, 2, 3].try_into().unwrap(),
1913+
);
17801914
let mut interceptor = ServiceCallInterceptor {
17811915
opts,
17821916
headers: headers.clone(),
@@ -1789,33 +1923,44 @@ mod tests {
17891923
req.metadata().get("authorization").unwrap(),
17901924
"Bearer my-api-key"
17911925
);
1926+
assert_eq!(
1927+
req.metadata().get_bin("my-bin-meta-key-bin").unwrap(),
1928+
vec![1, 2, 3].as_slice()
1929+
);
17921930

17931931
// Overwrite at request time
17941932
let mut req = tonic::Request::new(());
17951933
req.metadata_mut()
17961934
.insert("my-meta-key", "my-meta-val2".parse().unwrap());
17971935
req.metadata_mut()
17981936
.insert("authorization", "my-api-key2".parse().unwrap());
1937+
req.metadata_mut()
1938+
.insert_bin("my-bin-meta-key-bin", vec![4, 5, 6].try_into().unwrap());
17991939
let req = interceptor.call(req).unwrap();
18001940
assert_eq!(req.metadata().get("my-meta-key").unwrap(), "my-meta-val2");
18011941
assert_eq!(req.metadata().get("authorization").unwrap(), "my-api-key2");
1942+
assert_eq!(
1943+
req.metadata().get_bin("my-bin-meta-key-bin").unwrap(),
1944+
vec![4, 5, 6].as_slice()
1945+
);
18021946

18031947
// Overwrite auth on header
1804-
headers
1805-
.clone()
1806-
.write()
1807-
.user_headers
1808-
.insert("authorization".to_owned(), "my-api-key3".to_owned());
1948+
headers.clone().write().user_headers.insert(
1949+
"authorization".parse().unwrap(),
1950+
"my-api-key3".parse().unwrap(),
1951+
);
18091952
let req = interceptor.call(tonic::Request::new(())).unwrap();
18101953
assert_eq!(req.metadata().get("my-meta-key").unwrap(), "my-meta-val");
18111954
assert_eq!(req.metadata().get("authorization").unwrap(), "my-api-key3");
18121955

18131956
// Remove headers and auth and confirm gone
18141957
headers.clone().write().user_headers.clear();
1958+
headers.clone().write().user_binary_headers.clear();
18151959
headers.clone().write().api_key.take();
18161960
let req = interceptor.call(tonic::Request::new(())).unwrap();
18171961
assert!(!req.metadata().contains_key("my-meta-key"));
18181962
assert!(!req.metadata().contains_key("authorization"));
1963+
assert!(!req.metadata().contains_key("my-bin-meta-key-bin"));
18191964

18201965
// Timeout header not overriden
18211966
let mut req = tonic::Request::new(());
@@ -1828,6 +1973,55 @@ mod tests {
18281973
);
18291974
}
18301975

1976+
#[test]
1977+
fn invalid_ascii_header_key() {
1978+
let invalid_headers = {
1979+
let mut h = HashMap::new();
1980+
h.insert("x-binary-key-bin".to_owned(), "value".to_owned());
1981+
h
1982+
};
1983+
1984+
let result = parse_ascii_headers(invalid_headers);
1985+
assert!(result.is_err());
1986+
assert_eq!(
1987+
result.err().unwrap().to_string(),
1988+
"Invalid ASCII header key 'x-binary-key-bin': invalid gRPC metadata key name"
1989+
);
1990+
}
1991+
1992+
#[test]
1993+
fn invalid_ascii_header_value() {
1994+
let invalid_headers = {
1995+
let mut h = HashMap::new();
1996+
// Nul bytes are valid UTF-8, but not valid ascii gRPC headers:
1997+
h.insert("x-ascii-key".to_owned(), "\x00value".to_owned());
1998+
h
1999+
};
2000+
2001+
let result = parse_ascii_headers(invalid_headers);
2002+
assert!(result.is_err());
2003+
assert_eq!(
2004+
result.err().unwrap().to_string(),
2005+
"Invalid ASCII header value for key 'x-ascii-key': failed to parse metadata value"
2006+
);
2007+
}
2008+
2009+
#[test]
2010+
fn invalid_binary_header_key() {
2011+
let invalid_headers = {
2012+
let mut h = HashMap::new();
2013+
h.insert("x-ascii-key".to_owned(), vec![1, 2, 3]);
2014+
h
2015+
};
2016+
2017+
let result = parse_binary_headers(invalid_headers);
2018+
assert!(result.is_err());
2019+
assert_eq!(
2020+
result.err().unwrap().to_string(),
2021+
"Invalid binary header key 'x-ascii-key': invalid gRPC metadata key name"
2022+
);
2023+
}
2024+
18312025
#[test]
18322026
fn keep_alive_defaults() {
18332027
let mut builder = ClientOptionsBuilder::default();

core-c-bridge/src/client.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ pub extern "C" fn temporal_core_client_update_metadata(
240240
metadata: ByteArrayRef,
241241
) {
242242
let client = unsafe { &*client };
243-
client
243+
let _result = client
244244
.core
245245
.get_client()
246246
.set_headers(metadata.to_string_map_on_newlines());

tests/integ_tests/client_tests.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ async fn per_call_timeout_respected_whole_client() {
7777
let mut raw_client = opts.connect_no_namespace(None).await.unwrap();
7878
let mut hm = HashMap::new();
7979
hm.insert("grpc-timeout".to_string(), "0S".to_string());
80-
raw_client.get_client().set_headers(hm);
80+
raw_client.get_client().set_headers(hm).unwrap();
8181
let err = raw_client
8282
.describe_namespace(DescribeNamespaceRequest {
8383
namespace: NAMESPACE.to_string(),

0 commit comments

Comments
 (0)