1pub mod actions;
3#[cfg(not(target_arch = "wasm32"))]
4pub(crate) mod http1;
5mod reflection;
6mod streaming;
7
8pub use crate::server::grpc::value_codec::{
9 dynamic_message_to_json, json_to_dynamic_message, proto_value_to_json,
10};
11pub use actions::GrpcClientProtocol;
12
13use anyhow::{Context, Result};
14use bytes::Bytes;
15use http::Request;
16use http_body_util::BodyExt;
17use prost::Message as ProstMessage;
18use prost_reflect::{DescriptorPool, DynamicMessage};
19use std::net::SocketAddr;
20use std::sync::Arc;
21use tokio::sync::{mpsc, Mutex};
22use tonic::transport::{Channel, Endpoint};
23use tower::{Service, ServiceExt};
24use tracing::{debug, error, info};
25
26use crate::client::grpc::actions::{
27 GRPC_CLIENT_CONNECTED_EVENT, GRPC_CLIENT_ERROR_EVENT, GRPC_CLIENT_RESPONSE_RECEIVED_EVENT,
28};
29use crate::client::llm_budget::call_llm_for_client;
30use crate::llm::actions::client_trait::{Client as ClientTrait, ClientActionResult};
31use crate::llm::ollama_client::OllamaClient;
32use crate::llm::ClientLlmResult;
33use crate::protocol::{Event, StartupParams};
34use crate::state::app_state::AppState;
35use crate::state::{ClientId, ClientStatus};
36
37#[derive(Debug, Clone)]
39enum ConnectionState {
40 Idle,
41 Processing,
42}
43
44struct GrpcClientData {
46 channel: Channel,
47 descriptor_pool: Arc<DescriptorPool>,
48 state: ConnectionState,
49 automatic: mpsc::Sender<serde_json::Value>,
50}
51
52pub struct GrpcClient;
54pub const DEFAULT_TLS: bool = false;
55pub const CONNECT_TIMEOUT_SECS: u64 = 10;
56
57struct SocketGuard(std::net::TcpStream);
58impl Drop for SocketGuard {
59 fn drop(&mut self) {
60 let _ = self.0.shutdown(std::net::Shutdown::Both);
61 }
62}
63fn seconds(
64 params: Option<&StartupParams>,
65 key: &str,
66 default: u64,
67 maximum: u64,
68) -> Result<std::time::Duration> {
69 let seconds = params
70 .map(|params| params.get_optional_u64(key))
71 .transpose()?
72 .flatten()
73 .unwrap_or(default);
74 anyhow::ensure!(
75 (1..=maximum).contains(&seconds),
76 "{key} must be 1..{maximum}"
77 );
78 Ok(std::time::Duration::from_secs(seconds))
79}
80
81impl GrpcClient {
82 pub async fn connect_with_llm_actions(
84 remote_addr: String,
85 llm_client: OllamaClient,
86 app_state: Arc<AppState>,
87 status_tx: mpsc::UnboundedSender<String>,
88 client_id: ClientId,
89 startup_params: Option<StartupParams>,
90 ) -> Result<SocketAddr> {
91 let params = startup_params.as_ref();
92 let schema = params
93 .map(|p| p.get_optional_string("proto_schema"))
94 .transpose()?
95 .flatten();
96 let tls = params
97 .map(|p| p.get_optional_bool("use_tls"))
98 .transpose()?
99 .flatten()
100 .unwrap_or(DEFAULT_TLS);
101 let connect = seconds(params, "connect_timeout_secs", CONNECT_TIMEOUT_SECS, 60)?;
102 let stream_timeout = seconds(
103 params,
104 "stream_timeout_secs",
105 streaming::DEFAULT_STREAM_TIMEOUT,
106 3600,
107 )?;
108 let idle = seconds(
109 params,
110 "idle_timeout_secs",
111 streaming::DEFAULT_IDLE_TIMEOUT,
112 3600,
113 )?;
114 let name = params
115 .map(|p| p.get_optional_string("server_name"))
116 .transpose()?
117 .flatten();
118 let ca = params
119 .map(|p| p.get_optional_string("ca_file"))
120 .transpose()?
121 .flatten();
122 anyhow::ensure!(
123 tls || (name.is_none() && ca.is_none()),
124 "server_name/ca_file require use_tls"
125 );
126 let uri: hyper::Uri =
127 format!("{}://{}", if tls { "https" } else { "http" }, remote_addr).parse()?;
128 anyhow::ensure!(
129 uri.path() == "/" && uri.authority().is_some(),
130 "remote address must be host:port"
131 );
132 let host = uri.host().context("endpoint has no hostname")?.to_owned();
133 let port = uri.port_u16().unwrap_or(if tls { 443 } else { 80 });
134 let name = name.unwrap_or_else(|| host.clone());
135 anyhow::ensure!(
136 !name.is_empty() && name.len() <= 253,
137 "server_name too long or empty"
138 );
139 let (channel, descriptor_pool, socket, local, remote) =
140 tokio::time::timeout(connect, async {
141 let pem = if let Some(path) = ca {
142 Some(
143 tokio::task::spawn_blocking(move || -> Result<Vec<u8>> {
144 use std::io::Read;
145 let metadata = std::fs::metadata(&path)?;
146 anyhow::ensure!(
147 metadata.is_file() && metadata.len() <= 1024 * 1024,
148 "CA must be a regular file at most 1 MiB"
149 );
150 let mut options = std::fs::OpenOptions::new();
151 options.read(true);
152 #[cfg(unix)]
153 {
154 use std::os::unix::fs::OpenOptionsExt;
155 options.custom_flags(libc::O_NONBLOCK);
156 }
157 let file = options.open(path)?;
158 let metadata = file.metadata()?;
159 anyhow::ensure!(
160 metadata.is_file() && metadata.len() <= 1024 * 1024,
161 "CA must be a regular file at most 1 MiB"
162 );
163 let mut bytes = Vec::new();
164 file.take(1024 * 1024 + 1).read_to_end(&mut bytes)?;
165 anyhow::ensure!(bytes.len() <= 1024 * 1024, "CA exceeds 1 MiB");
166 Ok(bytes)
167 })
168 .await??,
169 )
170 } else {
171 None
172 };
173 let local_schema = if let Some(schema) = schema {
174 Some(load_schema(&schema).await?)
175 } else {
176 None
177 };
178 let stream = tokio::net::TcpStream::connect((host.as_str(), port)).await?;
179 let local = stream.local_addr()?;
180 let remote = stream.peer_addr()?;
181 let raw = stream.into_std()?;
182 let socket = SocketGuard(raw.try_clone()?);
183 let stream = tokio::net::TcpStream::from_std(raw)?;
184 let mut endpoint = Endpoint::from(uri)
185 .connect_timeout(connect)
186 .concurrency_limit(16)
187 .buffer_size(16)
188 .initial_stream_window_size(65536)
189 .initial_connection_window_size(1048576)
190 .http2_max_header_list_size(32768);
191 if tls {
192 let _ = rustls::crypto::ring::default_provider().install_default();
193 let mut config = tonic::transport::ClientTlsConfig::new()
194 .with_webpki_roots()
195 .domain_name(name);
196 if let Some(pem) = pem {
197 config =
198 config.ca_certificate(tonic::transport::Certificate::from_pem(pem));
199 }
200 endpoint = endpoint.tls_config(config)?;
201 }
202 let mut stream = Some(stream);
203 let connector = tower::service_fn(move |_: hyper::Uri| {
204 let stream = stream.take();
205 async move {
206 stream.map(hyper_util::rt::TokioIo::new).ok_or_else(|| {
207 std::io::Error::new(
208 std::io::ErrorKind::NotConnected,
209 "gRPC automatic reconnect is disabled",
210 )
211 })
212 }
213 });
214 let channel = endpoint.connect_with_connector(connector).await?;
215 let pool = match local_schema {
216 Some(pool) => pool,
217 None => reflection::discover(channel.clone()).await?,
218 };
219 anyhow::ensure!(
220 pool.files().len() <= 128 && pool.services().len() <= 128,
221 "schema exceeds 128 files/services"
222 );
223 anyhow::ensure!(
224 pool.file_descriptor_protos()
225 .map(ProstMessage::encoded_len)
226 .sum::<usize>()
227 <= 4 * 1024 * 1024,
228 "schema exceeds 4 MiB"
229 );
230 Ok::<_, anyhow::Error>((channel, pool, socket, local, remote))
231 })
232 .await
233 .context("gRPC connection/schema deadline exceeded")??;
234 let services: Vec<_> = descriptor_pool
235 .services()
236 .map(|service| service.full_name().to_owned())
237 .collect();
238 let (automatic, automatic_rx) = mpsc::channel(16);
239 let data = Arc::new(Mutex::new(GrpcClientData {
240 channel,
241 descriptor_pool: Arc::new(descriptor_pool),
242 state: ConnectionState::Idle,
243 automatic,
244 }));
245 let now = crate::utils::clock::Instant::now();
246 app_state
247 .with_client_mut(client_id, |client| {
248 client.connection = Some(crate::state::ClientConnectionState {
249 id: client_id,
250 remote_addr: remote_addr.clone(),
251 connected_addr: Some(remote),
252 local_addr: Some(local),
253 bytes_sent: 0,
254 bytes_received: 0,
255 packets_sent: 0,
256 packets_received: 0,
257 last_activity: now,
258 status: ClientStatus::Connected,
259 status_changed_at: now,
260 protocol_info: crate::state::server::ProtocolConnectionInfo::new(
261 serde_json::json!({"tls_verified":tls}),
262 ),
263 });
264 client.set_protocol_field("grpc_client".into(), serde_json::json!("initialized"));
265 client.set_protocol_field("server_addr".into(), serde_json::json!(remote_addr));
266 })
267 .await;
268 app_state
269 .update_client_status(client_id, ClientStatus::Connected)
270 .await;
271 let commands =
272 crate::client::command_support::register_command_channel(&app_state, client_id).await;
273 let connected = Event::new(
274 &GRPC_CLIENT_CONNECTED_EVENT,
275 serde_json::json!({"server_addr":remote_addr,"services":services}),
276 );
277 let ctx = streaming::ContextData {
278 id: client_id,
279 data,
280 state: app_state.clone(),
281 llm: llm_client,
282 status: status_tx.clone(),
283 protocol: Arc::new(GrpcClientProtocol::new()),
284 };
285 app_state
286 .spawn_client_task(
287 client_id,
288 streaming::run(
289 ctx,
290 socket,
291 commands,
292 automatic_rx,
293 connected,
294 stream_timeout,
295 idle,
296 ),
297 )
298 .await;
299 let _ = status_tx.send("__UPDATE_UI__".into());
300 Ok(local)
301 }
302}
303
304async fn load_schema(schema_input: &str) -> Result<DescriptorPool> {
306 crate::server::grpc::schema::load(schema_input).await
307}
308
309enum Applied {
311 Sent {
319 bytes_sent: usize,
320 pending_notify: Option<serde_json::Value>,
321 },
322 Ran(String),
324 Disconnect,
326}
327
328#[derive(Clone, Copy)]
330enum Dispatch {
331 Inline,
334 Silent,
339 Defer,
345}
346
347async fn execute_grpc_action(
349 client_id: ClientId,
350 action: serde_json::Value,
351 grpc_client_data: Arc<Mutex<GrpcClientData>>,
352 app_state: &Arc<AppState>,
353 llm_client: &OllamaClient,
354 status_tx: &mpsc::UnboundedSender<String>,
355 protocol: &Arc<GrpcClientProtocol>,
356 dispatch: Dispatch,
357) -> Result<Applied> {
358 let action_result = protocol.as_ref().execute_action(action.clone())?;
360 apply_grpc_action(
361 client_id,
362 action_result,
363 grpc_client_data,
364 app_state,
365 llm_client,
366 status_tx,
367 protocol,
368 dispatch,
369 )
370 .await
371}
372
373#[allow(clippy::too_many_arguments)]
376async fn apply_grpc_action(
377 client_id: ClientId,
378 action_result: ClientActionResult,
379 grpc_client_data: Arc<Mutex<GrpcClientData>>,
380 app_state: &Arc<AppState>,
381 llm_client: &OllamaClient,
382 status_tx: &mpsc::UnboundedSender<String>,
383 protocol: &Arc<GrpcClientProtocol>,
384 dispatch: Dispatch,
385) -> Result<Applied> {
386 match action_result {
387 ClientActionResult::Custom { name, data } if name.starts_with("grpc_stream_") => {
388 grpc_client_data
389 .lock()
390 .await
391 .automatic
392 .try_send(data)
393 .context("stream control queue full or closed")?;
394 Ok(Applied::Ran("stream control queued".into()))
395 }
396 ClientActionResult::Custom { name, data } if name == "grpc_call" => {
397 let service = data["service"]
398 .as_str()
399 .context("Missing service in grpc_call")?;
400 let method = data["method"]
401 .as_str()
402 .context("Missing method in grpc_call")?;
403 let request = &data["request"];
404 let metadata = data.get("metadata").and_then(|v| v.as_object());
405
406 let sent = make_grpc_call(
407 client_id,
408 service,
409 method,
410 request.clone(),
411 metadata.cloned(),
412 grpc_client_data,
413 app_state,
414 llm_client,
415 status_tx,
416 protocol,
417 dispatch,
418 )
419 .await?;
420
421 match sent {
422 Some(report) => Ok(Applied::Sent {
423 bytes_sent: report.bytes_sent,
424 pending_notify: report.pending_notify,
425 }),
426 None => Ok(Applied::Ran(format!(
429 "grpc_call {}/{} skipped: the client is already processing a call",
430 service, method
431 ))),
432 }
433 }
434 ClientActionResult::Disconnect => {
435 info!("gRPC client {} disconnecting", client_id);
436 app_state
437 .update_client_status(client_id, ClientStatus::Disconnected)
438 .await;
439 let _ = status_tx.send(format!("[CLIENT] gRPC client {} disconnected", client_id));
440 Ok(Applied::Disconnect)
441 }
442 ClientActionResult::WaitForMore => {
443 debug!("gRPC client {} waiting", client_id);
444 Ok(Applied::Ran("wait_for_more".to_string()))
445 }
446 ClientActionResult::NoAction => Ok(Applied::Ran("no_action".to_string())),
447 ClientActionResult::Custom { name, .. } => Ok(Applied::Ran(format!(
450 "custom result '{name}' is not handled by the gRPC client"
451 ))),
452 ClientActionResult::SendData(_) => Ok(Applied::Ran(
453 "send_data has no meaning for a gRPC client (tonic owns the HTTP/2 channel)"
454 .to_string(),
455 )),
456 ClientActionResult::Multiple(_) => Ok(Applied::Ran(
457 "multiple results are not produced by the gRPC client".to_string(),
458 )),
459 }
460}
461
462struct GrpcCallReport {
467 bytes_sent: usize,
469 pending_notify: Option<serde_json::Value>,
471}
472
473#[allow(clippy::too_many_arguments)]
486fn make_grpc_call<'a>(
487 client_id: ClientId,
488 service: &'a str,
489 method: &'a str,
490 request_json: serde_json::Value,
491 metadata: Option<serde_json::Map<String, serde_json::Value>>,
492 grpc_client_data: Arc<Mutex<GrpcClientData>>,
493 app_state: &'a Arc<AppState>,
494 llm_client: &'a OllamaClient,
495 status_tx: &'a mpsc::UnboundedSender<String>,
496 protocol: &'a Arc<GrpcClientProtocol>,
497 dispatch: Dispatch,
498) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Option<GrpcCallReport>>> + Send + 'a>>
499{
500 Box::pin(async move {
501 info!("gRPC client {} calling {}/{}", client_id, service, method);
502
503 let (input_desc, output_desc) = {
516 let data = grpc_client_data.lock().await;
517 let method_desc = data
518 .descriptor_pool
519 .get_service_by_name(service)
520 .and_then(|s| s.methods().find(|m| m.name() == method))
521 .context(format!("Method {}/{} not found in schema", service, method))?;
522 (method_desc.input(), method_desc.output())
523 };
524
525 let request_msg = json_to_dynamic_message(&request_json, &input_desc)
527 .context("Failed to convert request JSON to protobuf")?;
528
529 let request_bytes = request_msg.encode_to_vec();
531
532 info!(
533 "gRPC client {} sending {}-byte request to {}/{}",
534 client_id,
535 request_bytes.len(),
536 service,
537 method
538 );
539
540 let path = format!("/{}/{}", service, method);
542
543 use http::HeaderValue;
545
546 let mut request_builder = Request::builder()
547 .method("POST")
548 .uri(path.clone())
549 .header("content-type", "application/grpc")
550 .header("te", "trailers")
551 .header("grpc-accept-encoding", "identity");
552
553 if let Some(meta) = metadata {
555 for (key, value) in meta {
556 if let Some(val_str) = value.as_str() {
557 if let Ok(header_value) = HeaderValue::from_str(val_str) {
558 request_builder = request_builder.header(key.as_str(), header_value);
559 }
560 }
561 }
562 }
563
564 let mut grpc_message = Vec::with_capacity(5 + request_bytes.len());
566 grpc_message.push(0); grpc_message.extend_from_slice(
568 &u32::try_from(request_bytes.len())
569 .context("gRPC request length exceeds u32")?
570 .to_be_bytes(),
571 );
572 grpc_message.extend_from_slice(&request_bytes);
573
574 use http_body_util::combinators::UnsyncBoxBody;
576 let full_body = http_body_util::Full::new(Bytes::from(grpc_message));
577 let body =
578 UnsyncBoxBody::new(full_body.map_err(|_: std::convert::Infallible| {
579 tonic::Status::internal("infallible error")
580 }));
581 let http_request = request_builder
582 .body(body)
583 .context("Failed to build HTTP request")?;
584
585 let channel = {
588 let mut data = grpc_client_data.lock().await;
589 if matches!(data.state, ConnectionState::Processing) {
590 info!("gRPC client {} is busy, skipping request", client_id);
591 return Ok(None);
592 }
593 data.state = ConnectionState::Processing;
594 data.channel.clone()
595 };
596
597 let result = call_grpc_unary(&channel, http_request).await;
599
600 {
603 let mut data = grpc_client_data.lock().await;
604 data.state = ConnectionState::Idle;
605 }
606
607 let framed_len = 5 + request_bytes.len();
608
609 match result {
610 Ok(response_bytes) => {
611 let response_msg = DynamicMessage::decode(output_desc.clone(), &response_bytes[..])
613 .context("Failed to decode gRPC response")?;
614
615 let response_json = dynamic_message_to_json(&response_msg)?;
617
618 info!(
619 "gRPC client {} received response for {}/{}",
620 client_id, service, method
621 );
622
623 let event_data = serde_json::json!({
624 "service": service,
625 "method": method,
626 "response": response_json,
627 });
628
629 let pending_notify = match dispatch {
630 Dispatch::Inline => {
631 notify_grpc_response(
632 client_id,
633 event_data,
634 grpc_client_data,
635 app_state.clone(),
636 llm_client.clone(),
637 status_tx.clone(),
638 protocol.clone(),
639 )
640 .await;
641 None
642 }
643 Dispatch::Silent => None,
647 Dispatch::Defer => Some(event_data),
648 };
649
650 Ok(Some(GrpcCallReport {
651 bytes_sent: framed_len,
652 pending_notify,
653 }))
654 }
655 Err(e) => {
656 error!("gRPC client {} call failed: {}", client_id, e);
657
658 if matches!(dispatch, Dispatch::Silent) {
670 return Err(e);
672 }
673 if let Some(instruction) = app_state.get_instruction_for_client(client_id).await {
674 let event = Event::new(
675 &GRPC_CLIENT_ERROR_EVENT,
676 serde_json::json!({
677 "service": service,
678 "method": method,
679 "code": "UNKNOWN",
680 "message": e.to_string(),
681 }),
682 );
683
684 let memory = app_state
685 .get_memory_for_client(client_id)
686 .await
687 .unwrap_or_default();
688
689 match call_llm_for_client(
690 llm_client,
691 app_state,
692 client_id.to_string(),
693 &instruction,
694 &memory,
695 Some(&event),
696 protocol.as_ref(),
697 status_tx,
698 )
699 .await
700 {
701 Ok(result) => {
702 if let Some(mem) = result.memory_updates {
703 app_state.set_memory_for_client(client_id, mem).await;
704 }
705 for action in result.actions {
706 use crate::llm::actions::client_trait::Client;
707 match protocol.execute_action(action.clone()) {
708 Ok(ClientActionResult::Custom { name, data })
709 if name == "grpc_call" =>
710 {
711 let retry_service =
712 data["service"].as_str().unwrap_or(service).to_string();
713 let retry_method =
714 data["method"].as_str().unwrap_or(method).to_string();
715 info!(
716 "gRPC client {} retrying as {}/{} on the model's \
717 request",
718 client_id, retry_service, retry_method
719 );
720 let retry = make_grpc_call(
721 client_id,
722 &retry_service,
723 &retry_method,
724 data.get("request").cloned().unwrap_or_default(),
725 data.get("metadata")
726 .and_then(|m| m.as_object())
727 .cloned(),
728 grpc_client_data.clone(),
729 app_state,
730 llm_client,
731 status_tx,
732 protocol,
733 Dispatch::Silent,
734 )
735 .await;
736 if let Err(re) = retry {
737 error!(
738 "gRPC client {} retry also failed: {}",
739 client_id, re
740 );
741 }
742 }
743 Ok(ClientActionResult::Disconnect) => {
744 info!(
745 "gRPC client {} disconnecting after error, as the \
746 model asked",
747 client_id
748 );
749 app_state
750 .update_client_status(
751 client_id,
752 crate::state::ClientStatus::Disconnected,
753 )
754 .await;
755 let _ = status_tx.send("__UPDATE_UI__".to_string());
756 }
757 Ok(_) => {}
758 Err(ae) => error!(
759 "gRPC client {} rejected its own error-path action: {}",
760 client_id, ae
761 ),
762 }
763 }
764 }
765 Err(le) => error!(
766 "gRPC client {} LLM error on grpc_client_error: {}",
767 client_id, le
768 ),
769 }
770 }
771
772 Err(e)
773 }
774 }
775 })
776}
777
778#[allow(clippy::too_many_arguments)]
783async fn notify_grpc_response(
784 client_id: ClientId,
785 event_data: serde_json::Value,
786 grpc_client_data: Arc<Mutex<GrpcClientData>>,
787 app_state: Arc<AppState>,
788 llm_client: OllamaClient,
789 status_tx: mpsc::UnboundedSender<String>,
790 protocol: Arc<GrpcClientProtocol>,
791) {
792 let Some(instruction) = app_state.get_instruction_for_client(client_id).await else {
793 return;
794 };
795
796 let event = Event::new(&GRPC_CLIENT_RESPONSE_RECEIVED_EVENT, event_data);
797 let memory = app_state
798 .get_memory_for_client(client_id)
799 .await
800 .unwrap_or_default();
801
802 match call_llm_for_client(
803 &llm_client,
804 &app_state,
805 client_id.to_string(),
806 &instruction,
807 &memory,
808 Some(&event),
809 protocol.as_ref(),
810 &status_tx,
811 )
812 .await
813 {
814 Ok(ClientLlmResult {
815 actions,
816 memory_updates,
817 }) => {
818 if let Some(mem) = memory_updates {
820 app_state.set_memory_for_client(client_id, mem).await;
821 }
822
823 for action in actions {
825 if let Err(e) = Box::pin(execute_grpc_action(
826 client_id,
827 action,
828 grpc_client_data.clone(),
829 &app_state,
830 &llm_client,
831 &status_tx,
832 &protocol,
833 Dispatch::Inline,
834 ))
835 .await
836 {
837 error!("Failed to execute gRPC action: {}", e);
838 }
839 }
840 }
841 Err(e) => {
842 error!("LLM error for gRPC client {}: {}", client_id, e);
843 }
844 }
845}
846
847async fn call_grpc_unary(
849 channel: &Channel,
850 request: Request<http_body_util::combinators::UnsyncBoxBody<Bytes, tonic::Status>>,
851) -> Result<Vec<u8>> {
852 let mut client = channel.clone();
854
855 let response = client
857 .ready()
858 .await
859 .context("gRPC channel not ready")?
860 .call(request)
861 .await
862 .context("gRPC call failed")?;
863
864 let (parts, body) = response.into_parts();
873 let header_status = grpc_status_of(&parts.headers);
874
875 let collected = body
876 .collect()
877 .await
878 .context("Failed to read response body")?;
879 let trailers = collected.trailers().cloned();
880 let body_bytes = collected.to_bytes();
881
882 let (status_code, status_message) = match header_status {
883 Some(status) => (status, grpc_message_of(&parts.headers)),
884 None => match trailers.as_ref().and_then(grpc_status_of) {
885 Some(status) => (status, trailers.as_ref().and_then(grpc_message_of)),
886 None => (0, None),
889 },
890 };
891
892 if status_code != 0 {
893 return Err(anyhow::anyhow!(
894 "gRPC error: status={}, message={}",
895 status_code,
896 status_message.as_deref().unwrap_or("Unknown error")
897 ));
898 }
899
900 if body_bytes.len() < 5 {
902 return Err(anyhow::anyhow!("Response too short"));
903 }
904
905 let message_bytes = body_bytes.slice(5..);
906 Ok(message_bytes.to_vec())
907}
908
909fn grpc_status_of(headers: &http::HeaderMap) -> Option<i32> {
911 headers
912 .get("grpc-status")
913 .and_then(|v| v.to_str().ok())
914 .and_then(|s| s.trim().parse::<i32>().ok())
915}
916
917fn grpc_message_of(headers: &http::HeaderMap) -> Option<String> {
919 headers
920 .get("grpc-message")
921 .and_then(|v| v.to_str().ok())
922 .map(|s| s.to_string())
923}