Skip to content
This repository was archived by the owner on Apr 4, 2026. It is now read-only.

Commit 0921fa2

Browse files
fix: hub headers (#86)
* fix: hub headers * test: add edge-case tests for HeaderInterceptor Cover empty headers, multiple headers, metadata preservation across the interceptor boundary, and clone behavior to guard against regressions in the new interceptor path. --------- Co-authored-by: christopherwxyz <christopher@unofficial.run>
1 parent 432406b commit 0921fa2

4 files changed

Lines changed: 136 additions & 70 deletions

File tree

src/hub/client.rs

Lines changed: 39 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::proto::{FidsRequest, FidsResponse};
22
use crate::{
33
config::HubConfig,
4-
hub::stream::EventStream,
4+
hub::{HeaderInterceptor, stream::EventStream},
55
proto::{
66
BlocksRequest, GetInfoRequest, GetInfoResponse, ShardChunksRequest, ShardChunksResponse,
77
hub_service_client::HubServiceClient,
@@ -15,9 +15,14 @@ use std::{
1515
};
1616
use tokio_stream::Stream;
1717
use tonic::Status;
18+
use tonic::service::interceptor::InterceptedService;
1819
use tonic::transport::{Channel, ClientTlsConfig};
1920
use tracing::{error, info, warn};
2021

22+
/// Hub gRPC client configured with automatic custom header injection.
23+
pub type AuthenticatedHubServiceClient =
24+
HubServiceClient<InterceptedService<Channel, HeaderInterceptor>>;
25+
2126
#[derive(Debug, thiserror::Error)]
2227
pub enum Error {
2328
#[error("Connection error: {0}")]
@@ -124,8 +129,8 @@ impl HubRetryPolicy {
124129
pub struct Hub {
125130
// Use a Channel directly for building services with middleware
126131
channel: Option<Channel>,
127-
// Raw client without middleware
128-
client: Option<HubServiceClient<Channel>>,
132+
// Authenticated client with automatic header injection
133+
client: Option<AuthenticatedHubServiceClient>,
129134
config: Arc<HubConfig>,
130135
host: String,
131136
// Headers wrapped in Arc for cheap cloning in retry closures
@@ -156,9 +161,11 @@ impl Hub {
156161
})
157162
}
158163

159-
/// Add custom headers to request
160-
fn add_custom_headers<T>(&self, request: tonic::Request<T>) -> tonic::Request<T> {
161-
crate::hub::add_custom_headers(request, &self.headers)
164+
fn create_authenticated_client(
165+
channel: Channel,
166+
headers: Arc<HashMap<String, String>>,
167+
) -> AuthenticatedHubServiceClient {
168+
HubServiceClient::with_interceptor(channel, HeaderInterceptor::new(headers))
162169
}
163170

164171
/// Create an empty hub instance for testing or mock purposes
@@ -259,13 +266,13 @@ impl Hub {
259266
// Store the channel for future use
260267
self.channel = Some(channel.clone());
261268

262-
// Create base client without middleware for operations that don't need retries
263-
let client = HubServiceClient::new(channel.clone());
269+
// Create base client with automatic custom header injection
270+
let client = Self::create_authenticated_client(channel.clone(), Arc::clone(&self.headers));
264271
self.client = Some(client);
265272

266273
// Test connection with info request
267274
// Get hub info without middleware first time to avoid double retry
268-
let info_request = self.add_custom_headers(tonic::Request::new(GetInfoRequest {}));
275+
let info_request = tonic::Request::new(GetInfoRequest {});
269276
match self.client.as_mut().unwrap().get_info(info_request).await {
270277
Ok(response) => {
271278
let hub_info = response.into_inner();
@@ -385,7 +392,7 @@ impl Hub {
385392
}
386393
}
387394

388-
pub fn client(&mut self) -> Option<&mut HubServiceClient<Channel>> {
395+
pub fn client(&mut self) -> Option<&mut AuthenticatedHubServiceClient> {
389396
self.client.as_mut()
390397
}
391398

@@ -419,13 +426,11 @@ impl Hub {
419426
start_block_number: start_block,
420427
stop_block_number: end_block,
421428
});
422-
let request_with_headers = self.add_custom_headers(request);
423-
424429
// Get a mutable reference to the client
425430
let client = self.client.as_mut().ok_or(Error::NotConnected)?;
426431

427432
// Try to execute the request
428-
match client.get_blocks(request_with_headers).await {
433+
match client.get_blocks(request).await {
429434
Ok(response) => {
430435
// Reset error count and update success timestamp
431436
self.error_count.store(0, std::sync::atomic::Ordering::SeqCst);
@@ -496,14 +501,13 @@ impl Hub {
496501
let headers = Arc::clone(&headers);
497502
Box::pin(async move {
498503
let channel = channel.ok_or(Error::NotConnected)?;
499-
let mut client = HubServiceClient::new(channel);
504+
let mut client = Self::create_authenticated_client(channel, Arc::clone(&headers));
500505
let request = tonic::Request::new(ShardChunksRequest {
501506
shard_id,
502507
start_block_number: start_block,
503508
stop_block_number: end_block,
504509
});
505-
let request_with_headers = crate::hub::add_custom_headers(request, &headers);
506-
match client.get_shard_chunks(request_with_headers).await {
510+
match client.get_shard_chunks(request).await {
507511
Ok(response) => Ok(response.into_inner()),
508512
Err(status) => Err(Error::StatusError(status)),
509513
}
@@ -534,10 +538,9 @@ impl Hub {
534538
let headers = Arc::clone(&headers);
535539
Box::pin(async move {
536540
let channel = channel.ok_or(Error::NotConnected)?;
537-
let mut client = HubServiceClient::new(channel);
541+
let mut client = Self::create_authenticated_client(channel, Arc::clone(&headers));
538542
let request = tonic::Request::new(GetInfoRequest {});
539-
let request_with_headers = crate::hub::add_custom_headers(request, &headers);
540-
match client.get_info(request_with_headers).await {
543+
match client.get_info(request).await {
541544
Ok(response) => Ok(response.into_inner()),
542545
Err(status) => Err(Error::StatusError(status)),
543546
}
@@ -579,15 +582,14 @@ impl Hub {
579582
let headers = Arc::clone(&headers);
580583
Box::pin(async move {
581584
let channel = channel.ok_or(Error::NotConnected)?;
582-
let mut client = HubServiceClient::new(channel);
585+
let mut client = Self::create_authenticated_client(channel, Arc::clone(&headers));
583586
let request = tonic::Request::new(FidsRequest {
584587
page_size,
585588
page_token: page_token_clone,
586589
reverse,
587590
shard_id: 0,
588591
});
589-
let request_with_headers = crate::hub::add_custom_headers(request, &headers);
590-
match client.get_fids(request_with_headers).await {
592+
match client.get_fids(request).await {
591593
Ok(response) => Ok(response.into_inner()),
592594
Err(status) => Err(Error::StatusError(status)),
593595
}
@@ -614,10 +616,8 @@ impl Hub {
614616
let headers = Arc::clone(&headers);
615617
Box::pin(async move {
616618
let channel = channel.ok_or(Error::NotConnected)?;
617-
let mut client = HubServiceClient::new(channel);
618-
let request_with_headers =
619-
crate::hub::add_custom_headers(tonic::Request::new(request), &headers);
620-
match client.get_casts_by_fid(request_with_headers).await {
619+
let mut client = Self::create_authenticated_client(channel, Arc::clone(&headers));
620+
match client.get_casts_by_fid(tonic::Request::new(request)).await {
621621
Ok(response) => Ok(response.into_inner()),
622622
Err(status) => Err(Error::StatusError(status)),
623623
}
@@ -644,10 +644,8 @@ impl Hub {
644644
let headers = Arc::clone(&headers);
645645
Box::pin(async move {
646646
let channel = channel.ok_or(Error::NotConnected)?;
647-
let mut client = HubServiceClient::new(channel);
648-
let request_with_headers =
649-
crate::hub::add_custom_headers(tonic::Request::new(request), &headers);
650-
match client.get_reactions_by_fid(request_with_headers).await {
647+
let mut client = Self::create_authenticated_client(channel, Arc::clone(&headers));
648+
match client.get_reactions_by_fid(tonic::Request::new(request)).await {
651649
Ok(response) => Ok(response.into_inner()),
652650
Err(status) => Err(Error::StatusError(status)),
653651
}
@@ -674,10 +672,8 @@ impl Hub {
674672
let headers = Arc::clone(&headers);
675673
Box::pin(async move {
676674
let channel = channel.ok_or(Error::NotConnected)?;
677-
let mut client = HubServiceClient::new(channel);
678-
let request_with_headers =
679-
crate::hub::add_custom_headers(tonic::Request::new(request), &headers);
680-
match client.get_links_by_fid(request_with_headers).await {
675+
let mut client = Self::create_authenticated_client(channel, Arc::clone(&headers));
676+
match client.get_links_by_fid(tonic::Request::new(request)).await {
681677
Ok(response) => Ok(response.into_inner()),
682678
Err(status) => Err(Error::StatusError(status)),
683679
}
@@ -704,10 +700,8 @@ impl Hub {
704700
let headers = Arc::clone(&headers);
705701
Box::pin(async move {
706702
let channel = channel.ok_or(Error::NotConnected)?;
707-
let mut client = HubServiceClient::new(channel);
708-
let request_with_headers =
709-
crate::hub::add_custom_headers(tonic::Request::new(request), &headers);
710-
match client.get_verifications_by_fid(request_with_headers).await {
703+
let mut client = Self::create_authenticated_client(channel, Arc::clone(&headers));
704+
match client.get_verifications_by_fid(tonic::Request::new(request)).await {
711705
Ok(response) => Ok(response.into_inner()),
712706
Err(status) => Err(Error::StatusError(status)),
713707
}
@@ -734,10 +728,8 @@ impl Hub {
734728
let headers = Arc::clone(&headers);
735729
Box::pin(async move {
736730
let channel = channel.ok_or(Error::NotConnected)?;
737-
let mut client = HubServiceClient::new(channel);
738-
let request_with_headers =
739-
crate::hub::add_custom_headers(tonic::Request::new(request), &headers);
740-
match client.get_user_data_by_fid(request_with_headers).await {
731+
let mut client = Self::create_authenticated_client(channel, Arc::clone(&headers));
732+
match client.get_user_data_by_fid(tonic::Request::new(request)).await {
741733
Ok(response) => Ok(response.into_inner()),
742734
Err(status) => Err(Error::StatusError(status)),
743735
}
@@ -764,10 +756,8 @@ impl Hub {
764756
let headers = Arc::clone(&headers);
765757
Box::pin(async move {
766758
let channel = channel.ok_or(Error::NotConnected)?;
767-
let mut client = HubServiceClient::new(channel);
768-
let request_with_headers =
769-
crate::hub::add_custom_headers(tonic::Request::new(request), &headers);
770-
match client.get_all_user_data_messages_by_fid(request_with_headers).await {
759+
let mut client = Self::create_authenticated_client(channel, Arc::clone(&headers));
760+
match client.get_all_user_data_messages_by_fid(tonic::Request::new(request)).await {
771761
Ok(response) => Ok(response.into_inner()),
772762
Err(status) => Err(Error::StatusError(status)),
773763
}
@@ -824,10 +814,8 @@ impl Hub {
824814
let headers = Arc::clone(&headers);
825815
Box::pin(async move {
826816
let channel = channel.ok_or(Error::NotConnected)?;
827-
let mut client = HubServiceClient::new(channel);
828-
let request_with_headers =
829-
crate::hub::add_custom_headers(tonic::Request::new(request), &headers);
830-
match client.get_on_chain_events(request_with_headers).await {
817+
let mut client = Self::create_authenticated_client(channel, Arc::clone(&headers));
818+
match client.get_on_chain_events(tonic::Request::new(request)).await {
831819
Ok(response) => Ok(response.into_inner()),
832820
Err(status) => Err(Error::StatusError(status)),
833821
}
@@ -970,7 +958,7 @@ mod tests {
970958

971959
// Test that custom headers are properly added to requests
972960
let request = tonic::Request::new(GetInfoRequest {});
973-
let request_with_headers = hub.add_custom_headers(request);
961+
let request_with_headers = crate::hub::add_custom_headers(request, &hub.headers);
974962

975963
let metadata = request_with_headers.metadata();
976964
assert_eq!(metadata.get("x-api-key").unwrap().to_str().unwrap(), "test-key");

src/hub/mod.rs

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ pub mod subscriber;
1010
// Re-export data providers
1111
pub use providers::FarcasterHubClient;
1212

13-
use std::collections::HashMap;
13+
use std::{collections::HashMap, sync::Arc};
1414
use tonic::metadata::{MetadataKey, MetadataValue};
1515
use tracing::warn;
1616

@@ -56,10 +56,30 @@ pub fn add_custom_headers<T>(
5656
request
5757
}
5858

59+
/// gRPC interceptor that injects configured custom headers into every request.
60+
#[derive(Clone, Debug)]
61+
pub struct HeaderInterceptor {
62+
headers: Arc<HashMap<String, String>>,
63+
}
64+
65+
impl HeaderInterceptor {
66+
pub(crate) fn new(headers: Arc<HashMap<String, String>>) -> Self {
67+
Self { headers }
68+
}
69+
}
70+
71+
impl tonic::service::Interceptor for HeaderInterceptor {
72+
fn call(&mut self, request: tonic::Request<()>) -> Result<tonic::Request<()>, tonic::Status> {
73+
Ok(add_custom_headers(request, &self.headers))
74+
}
75+
}
76+
5977
#[cfg(test)]
6078
mod tests {
6179
use super::*;
80+
use std::sync::Arc;
6281
use tonic::Request;
82+
use tonic::service::Interceptor;
6383

6484
#[test]
6585
fn test_add_custom_headers_converts_underscores_to_hyphens() {
@@ -143,4 +163,71 @@ mod tests {
143163
);
144164
assert_eq!(metadata.get("x-api-version").unwrap().to_str().unwrap(), "v2.0");
145165
}
166+
167+
#[test]
168+
fn test_header_interceptor_adds_headers() {
169+
let mut headers = HashMap::new();
170+
headers.insert("X_API_KEY".to_string(), "test-key-123".to_string());
171+
172+
let mut interceptor = HeaderInterceptor::new(Arc::new(headers));
173+
let request = Request::new(());
174+
let request = interceptor.call(request).unwrap();
175+
176+
let metadata = request.metadata();
177+
assert_eq!(metadata.get("x-api-key").unwrap().to_str().unwrap(), "test-key-123");
178+
}
179+
180+
#[test]
181+
fn test_header_interceptor_empty_headers() {
182+
let mut interceptor = HeaderInterceptor::new(Arc::new(HashMap::new()));
183+
let request = Request::new(());
184+
let request = interceptor.call(request).unwrap();
185+
186+
assert_eq!(request.metadata().len(), 0);
187+
}
188+
189+
#[test]
190+
fn test_header_interceptor_multiple_headers() {
191+
let mut headers = HashMap::new();
192+
headers.insert("X_API_KEY".to_string(), "key-abc".to_string());
193+
headers.insert("AUTHORIZATION".to_string(), "Bearer tok".to_string());
194+
headers.insert("X_CUSTOM".to_string(), "val".to_string());
195+
196+
let mut interceptor = HeaderInterceptor::new(Arc::new(headers));
197+
let request = Request::new(());
198+
let request = interceptor.call(request).unwrap();
199+
200+
let md = request.metadata();
201+
assert_eq!(md.get("x-api-key").unwrap().to_str().unwrap(), "key-abc");
202+
assert_eq!(md.get("authorization").unwrap().to_str().unwrap(), "Bearer tok");
203+
assert_eq!(md.get("x-custom").unwrap().to_str().unwrap(), "val");
204+
}
205+
206+
#[test]
207+
fn test_header_interceptor_preserves_existing_metadata() {
208+
let mut headers = HashMap::new();
209+
headers.insert("X_API_KEY".to_string(), "injected".to_string());
210+
211+
let mut interceptor = HeaderInterceptor::new(Arc::new(headers));
212+
let mut request = Request::new(());
213+
request.metadata_mut().insert("pre-existing", MetadataValue::from_static("original"));
214+
215+
let request = interceptor.call(request).unwrap();
216+
let md = request.metadata();
217+
assert_eq!(md.get("pre-existing").unwrap().to_str().unwrap(), "original");
218+
assert_eq!(md.get("x-api-key").unwrap().to_str().unwrap(), "injected");
219+
}
220+
221+
#[test]
222+
fn test_header_interceptor_is_clone() {
223+
let mut headers = HashMap::new();
224+
headers.insert("X_KEY".to_string(), "val".to_string());
225+
226+
let interceptor = HeaderInterceptor::new(Arc::new(headers));
227+
let mut cloned = interceptor.clone();
228+
229+
// Both should produce identical results
230+
let request = cloned.call(Request::new(())).unwrap();
231+
assert_eq!(request.metadata().get("x-key").unwrap().to_str().unwrap(), "val");
232+
}
146233
}

src/hub/stream.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
use crate::proto::hub_service_client::HubServiceClient;
2-
use tonic::transport::Channel;
1+
use crate::hub::client::AuthenticatedHubServiceClient;
32

43
pub struct EventStream {
5-
_client: HubServiceClient<Channel>,
4+
_client: AuthenticatedHubServiceClient,
65
}
76

87
impl EventStream {
9-
pub fn new(client: &mut HubServiceClient<Channel>) -> Self {
8+
pub fn new(client: &mut AuthenticatedHubServiceClient) -> Self {
109
Self { _client: client.clone() }
1110
}
1211
}

0 commit comments

Comments
 (0)