1pub mod actions;
7#[cfg(not(target_arch = "wasm32"))]
8pub mod http1;
9mod reflection;
10pub(crate) mod schema;
11pub mod stream_codec;
12pub(crate) mod streaming;
13pub mod value_codec;
14pub use value_codec::{dynamic_message_to_json, json_to_dynamic_message, proto_value_to_json};
15
16use anyhow::{bail, Context, Result};
17use std::net::SocketAddr;
18use std::sync::Arc;
19use tokio::sync::mpsc;
20use tracing::{debug, info, trace, warn};
21
22#[cfg(not(target_arch = "wasm32"))]
23use crate::llm::action_helper::call_llm;
24#[cfg(not(target_arch = "wasm32"))]
25use crate::llm::ollama_client::OllamaClient;
26#[cfg(not(target_arch = "wasm32"))]
27use crate::llm::ActionResult;
28#[cfg(not(target_arch = "wasm32"))]
29use crate::logging::emit::Log;
30#[cfg(not(target_arch = "wasm32"))]
31use crate::protocol::Event;
32#[cfg(not(target_arch = "wasm32"))]
33use crate::server::grpc::actions::GRPC_UNARY_REQUEST_EVENT;
34#[cfg(not(target_arch = "wasm32"))]
35use crate::server::GrpcProtocol;
36#[cfg(not(target_arch = "wasm32"))]
37use crate::state::app_state::AppState;
38use crate::{console_error, console_info};
39#[cfg(not(target_arch = "wasm32"))]
40use bytes::Bytes;
41#[cfg(not(target_arch = "wasm32"))]
42use http_body_util::{BodyExt, Limited};
43#[cfg(not(target_arch = "wasm32"))]
44use hyper::{body::Incoming, header::HeaderValue, Request, Response, StatusCode};
45#[cfg(not(target_arch = "wasm32"))]
46use prost::Message;
47#[cfg(not(target_arch = "wasm32"))]
48use prost_reflect::{DescriptorPool, DynamicMessage};
49#[cfg(not(target_arch = "wasm32"))]
50use serde_json::json;
51
52#[cfg(not(target_arch = "wasm32"))]
55pub const MAX_REQUEST_BYTES: usize = 4 * 1024 * 1024;
56
57#[cfg(not(target_arch = "wasm32"))]
66const FIRST_BYTE_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
67
68#[cfg(not(target_arch = "wasm32"))]
75const IDLE_BETWEEN_REQUESTS_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(900);
76
77#[cfg(not(target_arch = "wasm32"))]
83const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
84
85#[cfg(not(target_arch = "wasm32"))]
94const CONNECTION_CAP_REFUSAL: &[u8] = b"HTTP/1.1 503 Service Unavailable\r\n\
95 Content-Length: 0\r\nRetry-After: 5\r\nConnection: close\r\n\r\n";
96
97#[cfg(not(target_arch = "wasm32"))]
104#[derive(Clone, Copy, Debug, PartialEq, Eq)]
105#[repr(i32)]
106pub enum GrpcStatus {
107 Ok = 0,
108 Cancelled = 1,
109 Unknown = 2,
110 InvalidArgument = 3,
111 DeadlineExceeded = 4,
112 NotFound = 5,
113 AlreadyExists = 6,
114 PermissionDenied = 7,
115 ResourceExhausted = 8,
116 FailedPrecondition = 9,
117 Aborted = 10,
118 OutOfRange = 11,
119 Unimplemented = 12,
120 Internal = 13,
121 Unavailable = 14,
122 DataLoss = 15,
123 Unauthenticated = 16,
124}
125
126#[cfg(not(target_arch = "wasm32"))]
127impl GrpcStatus {
128 fn parse(code: &str) -> Self {
132 if let Ok(n) = code.trim().parse::<i32>() {
133 return Self::from_i32(n);
134 }
135 match code.trim().to_ascii_uppercase().replace('-', "_").as_str() {
136 "OK" => Self::Ok,
137 "CANCELLED" | "CANCELED" => Self::Cancelled,
138 "UNKNOWN" => Self::Unknown,
139 "INVALID_ARGUMENT" => Self::InvalidArgument,
140 "DEADLINE_EXCEEDED" => Self::DeadlineExceeded,
141 "NOT_FOUND" => Self::NotFound,
142 "ALREADY_EXISTS" => Self::AlreadyExists,
143 "PERMISSION_DENIED" => Self::PermissionDenied,
144 "RESOURCE_EXHAUSTED" => Self::ResourceExhausted,
145 "FAILED_PRECONDITION" => Self::FailedPrecondition,
146 "ABORTED" => Self::Aborted,
147 "OUT_OF_RANGE" => Self::OutOfRange,
148 "UNIMPLEMENTED" | "NOT_IMPLEMENTED" => Self::Unimplemented,
149 "INTERNAL" => Self::Internal,
150 "UNAVAILABLE" => Self::Unavailable,
151 "DATA_LOSS" => Self::DataLoss,
152 "UNAUTHENTICATED" => Self::Unauthenticated,
153 _ => Self::Unknown,
154 }
155 }
156
157 fn from_i32(n: i32) -> Self {
158 match n {
159 0 => Self::Ok,
160 1 => Self::Cancelled,
161 2 => Self::Unknown,
162 3 => Self::InvalidArgument,
163 4 => Self::DeadlineExceeded,
164 5 => Self::NotFound,
165 6 => Self::AlreadyExists,
166 7 => Self::PermissionDenied,
167 8 => Self::ResourceExhausted,
168 9 => Self::FailedPrecondition,
169 10 => Self::Aborted,
170 11 => Self::OutOfRange,
171 12 => Self::Unimplemented,
172 14 => Self::Unavailable,
173 15 => Self::DataLoss,
174 16 => Self::Unauthenticated,
175 _ => Self::Internal,
176 }
177 }
178}
179
180#[cfg(not(target_arch = "wasm32"))]
196pub fn grpc_status_for_llm_failure(err: &anyhow::Error) -> GrpcStatus {
197 if crate::llm::is_overload_error(err) {
198 GrpcStatus::Unavailable
199 } else {
200 GrpcStatus::Internal
201 }
202}
203
204#[cfg(not(target_arch = "wasm32"))]
206pub fn grpc_status_for_value_failure(error: &anyhow::Error) -> GrpcStatus {
207 if error
208 .downcast_ref::<value_codec::ValueLimitExceeded>()
209 .is_some()
210 {
211 GrpcStatus::ResourceExhausted
212 } else {
213 GrpcStatus::InvalidArgument
214 }
215}
216
217#[cfg(not(target_arch = "wasm32"))]
227fn header_safe(message: &str) -> String {
228 let mut out = String::with_capacity(message.len().min(512));
229 let mut last_was_space = false;
230 for c in message.chars() {
231 let c = if (' '..='~').contains(&c) { c } else { ' ' };
232 if c == ' ' {
233 if last_was_space {
234 continue;
235 }
236 last_was_space = true;
237 } else {
238 last_was_space = false;
239 }
240 if out.chars().count() >= 512 {
241 break;
242 }
243 out.push(c);
244 }
245 let trimmed = out.trim();
246 if trimmed.is_empty() {
247 "internal error".to_string()
248 } else {
249 trimmed.to_string()
250 }
251}
252
253#[cfg(not(target_arch = "wasm32"))]
255struct GrpcFailure {
256 status: GrpcStatus,
257 message: String,
258}
259
260#[cfg(not(target_arch = "wasm32"))]
261impl GrpcFailure {
262 fn new(status: GrpcStatus, message: impl Into<String>) -> Self {
263 Self {
264 status,
265 message: message.into(),
266 }
267 }
268}
269
270#[cfg(not(target_arch = "wasm32"))]
271impl From<anyhow::Error> for GrpcFailure {
272 fn from(e: anyhow::Error) -> Self {
273 let status = grpc_status_for_llm_failure(&e);
281 Self::new(status, crate::utils::WireFailure::classify(&e).text())
282 }
283}
284
285#[cfg(not(target_arch = "wasm32"))]
286impl From<serde_json::Error> for GrpcFailure {
287 fn from(e: serde_json::Error) -> Self {
288 debug!("gRPC JSON conversion failed: {e}");
290 Self::new(GrpcStatus::Internal, "request could not be processed")
291 }
292}
293
294#[cfg(not(target_arch = "wasm32"))]
295impl From<prost::EncodeError> for GrpcFailure {
296 fn from(e: prost::EncodeError) -> Self {
297 debug!("gRPC response encoding failed: {e}");
298 Self::new(GrpcStatus::Internal, "could not encode response")
299 }
300}
301
302pub struct GrpcServer;
304
305#[cfg(not(target_arch = "wasm32"))]
306impl GrpcServer {
307 pub async fn spawn_with_llm_actions(
312 listen_addr: SocketAddr,
313 llm_client: OllamaClient,
314 app_state: Arc<AppState>,
315 status_tx: mpsc::UnboundedSender<String>,
316 server_id: crate::state::ServerId,
317 startup_params: Option<crate::protocol::StartupParams>,
318 ) -> Result<SocketAddr> {
319 let proto_schema = startup_params
321 .as_ref()
322 .map(|p| p.get_string("proto_schema"))
323 .transpose()?
324 .context(
325 "Missing 'proto_schema' in startup_params. LLM must provide protobuf definition.",
326 )?;
327
328 trace!("Proto schema:\n{}", proto_schema);
329 Log::new(Some(&status_tx)).debug("Compiling protobuf schema for gRPC server");
330
331 let descriptor_pool = schema::load(&proto_schema)
332 .await
333 .context("Failed to compile/load protobuf schema")?;
334 let reflection_enabled = startup_params
335 .as_ref()
336 .map(|p| p.get_optional_bool("enable_reflection"))
337 .transpose()?
338 .flatten()
339 .unwrap_or(streaming::DEFAULT_REFLECTION);
340 let timeout_secs = startup_params
341 .as_ref()
342 .map(|p| p.get_optional_u64("stream_timeout_secs"))
343 .transpose()?
344 .flatten()
345 .unwrap_or(streaming::DEFAULT_TIMEOUT_SECS);
346 anyhow::ensure!(
347 (1..=3600).contains(&timeout_secs),
348 "stream_timeout_secs must be 1..3600"
349 );
350 let reflection = if reflection_enabled {
351 Some(reflection::Reflection::new(&descriptor_pool)?)
352 } else {
353 None
354 };
355
356 let services = descriptor_pool.services().collect::<Vec<_>>();
357
358 if services.is_empty() {
359 bail!("No services found in protobuf schema. Schema must define at least one service.");
360 }
361
362 info!("gRPC server starting with {} service(s)", services.len());
363 let log = Log::new(Some(&status_tx));
364 for service in &services {
365 log.info(format!(
366 "gRPC service: {} ({} methods)",
367 service.full_name(),
368 service.methods().count()
369 ));
370 }
371
372 let protocol = Arc::new(GrpcProtocol::new());
374 let descriptor_pool_arc = Arc::new(descriptor_pool.clone());
375
376 let dynamic_service = DynamicGrpcService {
378 llm_client: llm_client.clone(),
379 app_state: app_state.clone(),
380 status_tx: status_tx.clone(),
381 server_id,
382 descriptor_pool: descriptor_pool_arc,
383 protocol,
384 reflection,
385 stream_timeout: std::time::Duration::from_secs(timeout_secs),
386 streams: Arc::new(tokio::sync::Semaphore::new(streaming::MAX_ACTIVE)),
387 next_stream_id: Arc::new(std::sync::atomic::AtomicU32::new(1)),
388 };
389
390 let listener =
392 crate::server::socket_helpers::create_reusable_tcp_listener(listen_addr).await?;
393 let actual_addr = listener.local_addr()?;
394
395 console_info!(status_tx, "gRPC server listening on {}", actual_addr);
396
397 let service = Arc::new(dynamic_service);
399 let task_registrar = app_state.clone();
400 let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
401 let accept_handle = tokio::spawn(async move {
402 loop {
403 match crate::server::accept_bounded::accept_bounded(
404 &listener,
405 &limiter,
406 CONNECTION_CAP_REFUSAL,
407 "gRPC",
408 Some(&status_tx),
409 )
410 .await
411 {
412 Ok((stream, remote_addr, permit)) => {
413 let connection_id = crate::server::connection::ConnectionId::new(
414 service.app_state.get_next_unified_id().await,
415 );
416 debug!("gRPC connection {} from {}", connection_id, remote_addr);
417 Log::new(Some(&status_tx))
418 .debug(format!("gRPC connection from {}", remote_addr));
419
420 use crate::state::server::{
422 ConnectionState as ServerConnectionState, ConnectionStatus,
423 ProtocolConnectionInfo,
424 };
425 let now = crate::utils::clock::Instant::now();
426 let conn_state = ServerConnectionState {
427 id: connection_id,
428 remote_addr,
429 local_addr: actual_addr,
430 bytes_sent: 0,
431 bytes_received: 0,
432 packets_sent: 0,
433 packets_received: 0,
434 last_activity: now,
435 status: ConnectionStatus::Active,
436 status_changed_at: now,
437 protocol_info: ProtocolConnectionInfo::empty(),
438 };
439 app_state
440 .add_connection_to_server(server_id, conn_state)
441 .await;
442 let _ = status_tx.send("__UPDATE_UI__".to_string());
443
444 let service_clone = service.clone();
445 let app_state_clone = app_state.clone();
446 let status_tx_clone = status_tx.clone();
447
448 let task_owner = app_state.clone();
451 task_owner
452 .spawn_server_task(server_id, async move {
453 let _permit = permit;
457
458 match tokio::time::timeout(
465 FIRST_BYTE_READ_TIMEOUT,
466 stream.peek(&mut [0u8; 1]),
467 )
468 .await
469 {
470 Ok(Ok(0)) | Ok(Err(_)) => {
471 app_state_clone
472 .remove_connection_from_server(server_id, connection_id)
473 .await;
474 let _ = status_tx_clone.send("__UPDATE_UI__".to_string());
475 return;
476 }
477 Ok(Ok(_)) => {}
478 Err(_) => {
479 Log::new(Some(&status_tx_clone)).debug(format!(
480 "gRPC peer {} sent nothing for {}s; closing before the \
481 HTTP/2 preface",
482 remote_addr,
483 FIRST_BYTE_READ_TIMEOUT.as_secs()
484 ));
485 app_state_clone
486 .remove_connection_from_server(server_id, connection_id)
487 .await;
488 let _ = status_tx_clone.send("__UPDATE_UI__".to_string());
489 return;
490 }
491 }
492
493 let io = hyper_util::rt::TokioIo::new(stream);
494
495 let activity = Arc::new(
501 crate::server::accept_bounded::ConnectionActivity::new(),
502 );
503 let activity_for_service = Arc::clone(&activity);
504 let executor = streaming::OwnedExecutor::default();
505 let _children = streaming::ConnectionTasks(executor.clone());
506 let registry = streaming::Registry::default();
507 let peer_rx = crate::server::peer_support::register_peer_channel(
508 &app_state_clone,
509 server_id,
510 connection_id.as_u32(),
511 )
512 .await;
513 let _ = executor.spawn(streaming::command_loop(
514 peer_rx,
515 registry.clone(),
516 service_clone.clone(),
517 connection_id,
518 ));
519 let executor_for_service = executor.clone();
520
521 let grpc_service = hyper::service::service_fn(move |req| {
523 let service = service_clone.clone();
524 let conn_id = connection_id;
525 let activity = Arc::clone(&activity_for_service);
526 let registry = registry.clone();
527 let executor = executor_for_service.clone();
528 async move {
529 let busy = activity.busy();
530 service
531 .handle_grpc_request(
532 req, conn_id, busy, registry, executor,
533 )
534 .await
535 }
536 });
537
538 let conn = hyper::server::conn::http2::Builder::new(executor)
540 .max_concurrent_streams(16)
541 .initial_stream_window_size(65536)
542 .initial_connection_window_size(1048576)
543 .max_header_list_size(32768)
544 .max_frame_size(16384)
545 .serve_connection(io, grpc_service);
546 tokio::pin!(conn);
547 tokio::select! {
548 result = &mut conn => {
549 if let Err(e) = result {
550 Log::new(Some(&status_tx_clone))
551 .debug(format!("gRPC connection error: {}", e));
552 }
553 }
554 _ = crate::server::accept_bounded::watch_idle(
555 Arc::clone(&activity),
556 IDLE_BETWEEN_REQUESTS_TIMEOUT,
557 ) => {
558 debug!(
559 "gRPC connection {} idle for {}s; closing",
560 connection_id,
561 IDLE_BETWEEN_REQUESTS_TIMEOUT.as_secs()
562 );
563 }
564 }
565
566 app_state_clone
568 .remove_peer_handle(server_id, connection_id.as_u32())
569 .await;
570 app_state_clone
571 .remove_connection_from_server(server_id, connection_id)
572 .await;
573 let _ = status_tx_clone.send("__UPDATE_UI__".to_string());
574 })
575 .await;
576 }
577 Err(e) => {
578 console_error!(status_tx, "Failed to accept gRPC connection: {}", e);
579 break;
580 }
581 }
582 }
583 });
584
585 task_registrar
586 .register_server_task(server_id, accept_handle)
587 .await;
588
589 Ok(actual_addr)
590 }
591}
592
593#[cfg(not(target_arch = "wasm32"))]
595#[derive(Clone)]
596struct DynamicGrpcService {
597 llm_client: OllamaClient,
598 app_state: Arc<AppState>,
599 status_tx: mpsc::UnboundedSender<String>,
600 server_id: crate::state::ServerId,
601 descriptor_pool: Arc<DescriptorPool>,
602 protocol: Arc<dyn crate::llm::actions::protocol_trait::Server>,
603 reflection: Option<Arc<reflection::Reflection>>,
604 stream_timeout: std::time::Duration,
605 streams: Arc<tokio::sync::Semaphore>,
606 next_stream_id: Arc<std::sync::atomic::AtomicU32>,
607}
608
609#[cfg(not(target_arch = "wasm32"))]
612#[derive(Clone)]
613pub(crate) struct HttpCore(Arc<DynamicGrpcService>);
614#[cfg(not(target_arch = "wasm32"))]
615pub(crate) struct WebAdmission {
616 pub(crate) deadline: tokio::time::Instant,
617 permit: tokio::sync::OwnedSemaphorePermit,
618 timer: streaming::DeadlineGuard,
619 pub(crate) method: prost_reflect::MethodDescriptor,
620 id: u32,
621}
622#[cfg(not(target_arch = "wasm32"))]
623impl WebAdmission {
624 pub(crate) fn guard(
626 self,
627 response: Response<tonic::body::BoxBody>,
628 busy: crate::server::accept_bounded::BusyGuard,
629 ) -> Response<tonic::body::BoxBody> {
630 response.map(|body| http1::hold_body(body, (self.permit, busy, self.timer)))
631 }
632}
633#[cfg(not(target_arch = "wasm32"))]
634impl HttpCore {
635 pub(crate) fn new(
636 ctx: &crate::protocol::SpawnContext,
637 pool: DescriptorPool,
638 protocol: Arc<dyn crate::llm::actions::protocol_trait::Server>,
639 timeout: std::time::Duration,
640 ) -> Self {
641 Self(Arc::new(DynamicGrpcService {
642 llm_client: ctx.llm_client.clone(),
643 app_state: ctx.state.clone(),
644 status_tx: ctx.status_tx.clone(),
645 server_id: ctx.server_id,
646 descriptor_pool: Arc::new(pool),
647 protocol,
648 reflection: None,
649 stream_timeout: timeout,
650 streams: Arc::new(tokio::sync::Semaphore::new(streaming::MAX_ACTIVE)),
651 next_stream_id: Arc::new(std::sync::atomic::AtomicU32::new(1)),
652 }))
653 }
654 pub(crate) async fn commands(
655 self,
656 commands: mpsc::Receiver<crate::state::client_handles::ClientCommand>,
657 registry: streaming::Registry,
658 connection: crate::server::connection::ConnectionId,
659 ) {
660 streaming::command_loop(commands, registry, self.0, connection).await;
661 }
662 pub(crate) fn prepare(
663 &self,
664 request: &Request<tonic::body::BoxBody>,
665 executor: streaming::OwnedExecutor,
666 ) -> Result<WebAdmission, tonic::Status> {
667 let method = DynamicGrpcService::parse_grpc_path(request.uri().path())
668 .ok()
669 .and_then(|(service, method)| {
670 self.0
671 .descriptor_pool
672 .get_service_by_name(&service)
673 .and_then(|service| {
674 service
675 .methods()
676 .find(|candidate| candidate.name() == method)
677 })
678 });
679 let method = method.ok_or_else(|| tonic::Status::unimplemented("unknown RPC method"))?;
680 if method.is_client_streaming() {
681 return Err(tonic::Status::unimplemented(
682 "HTTP/1 RPC binding supports unary and server-streaming methods only",
683 ));
684 }
685 let timeout = streaming::timeout(request.headers(), self.0.stream_timeout)?;
686 let deadline = tokio::time::Instant::now() + timeout;
687 let permit = self
688 .0
689 .streams
690 .clone()
691 .try_acquire_owned()
692 .map_err(|_| tonic::Status::unavailable("server at RPC capacity"))?;
693 let timer = executor.deadline(deadline)?.retain_expired();
694 let id = match self.0.next_stream_id.fetch_update(
695 std::sync::atomic::Ordering::Relaxed,
696 std::sync::atomic::Ordering::Relaxed,
697 |id| id.checked_add(1),
698 ) {
699 Ok(id) => id,
700 Err(_) => {
701 return Err(tonic::Status::resource_exhausted(
702 "RPC identifiers exhausted",
703 ))
704 }
705 };
706 Ok(WebAdmission {
707 deadline,
708 permit,
709 timer,
710 method,
711 id,
712 })
713 }
714 pub(crate) async fn request(
715 &self,
716 request: Request<tonic::body::BoxBody>,
717 connection: crate::server::connection::ConnectionId,
718 registry: streaming::Registry,
719 admission: &WebAdmission,
720 ) -> Response<tonic::body::BoxBody> {
721 streaming::dispatch(
722 request,
723 self.0.clone(),
724 connection,
725 admission.method.clone(),
726 registry,
727 admission.deadline,
728 admission.id,
729 )
730 .await
731 }
732}
733
734#[cfg(not(target_arch = "wasm32"))]
754type GrpcBody = tonic::body::BoxBody;
755
756#[cfg(not(target_arch = "wasm32"))]
760fn grpc_body_with_trailers(body: Bytes, status: i32, message: &str) -> GrpcBody {
761 use http_body_util::StreamBody;
762
763 let mut trailers = hyper::HeaderMap::new();
764 trailers.insert(
765 "grpc-status",
766 HeaderValue::from_str(&status.to_string())
767 .unwrap_or_else(|_| HeaderValue::from_static("13")),
768 );
769 trailers.insert(
770 "grpc-message",
771 HeaderValue::from_str(message).unwrap_or_else(|_| HeaderValue::from_static("")),
772 );
773
774 let frames: Vec<std::result::Result<hyper::body::Frame<Bytes>, tonic::Status>> = vec![
775 Ok(hyper::body::Frame::data(body)),
776 Ok(hyper::body::Frame::trailers(trailers)),
777 ];
778
779 BodyExt::boxed_unsync(StreamBody::new(futures::stream::iter(frames)))
780}
781
782#[cfg(not(target_arch = "wasm32"))]
784fn empty_grpc_body() -> GrpcBody {
785 http_body_util::Empty::<Bytes>::new()
786 .map_err(|never| match never {})
787 .boxed_unsync()
788}
789
790#[cfg(not(target_arch = "wasm32"))]
791impl DynamicGrpcService {
792 async fn handle_grpc_request(
794 self: Arc<Self>,
795 req: Request<Incoming>,
796 connection_id: crate::server::connection::ConnectionId,
797 busy: crate::server::accept_bounded::BusyGuard,
798 registry: streaming::Registry,
799 executor: streaming::OwnedExecutor,
800 ) -> Result<Response<GrpcBody>, hyper::Error> {
801 let reflection = matches!(
802 req.uri().path(),
803 "/grpc.reflection.v1.ServerReflection/ServerReflectionInfo"
804 | "/grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo"
805 );
806 let method = Self::parse_grpc_path(req.uri().path())
807 .ok()
808 .and_then(|(service, method)| {
809 self.descriptor_pool
810 .get_service_by_name(&service)
811 .and_then(|service| {
812 service
813 .methods()
814 .find(|candidate| candidate.name() == method)
815 })
816 });
817 let streaming_method =
818 method.filter(|method| method.is_client_streaming() || method.is_server_streaming());
819 if reflection || streaming_method.is_some() {
820 if req.method() != hyper::Method::POST
821 || req.version() != hyper::Version::HTTP_2
822 || !req.headers().get("content-type").is_some_and(|value| {
823 value
824 .to_str()
825 .is_ok_and(|value| value.starts_with("application/grpc"))
826 })
827 {
828 return Ok(tonic::Status::invalid_argument(
829 "streaming requires POST application/grpc over HTTP/2",
830 )
831 .into_http());
832 }
833 let maximum = if reflection {
834 std::time::Duration::from_secs(30)
835 } else {
836 self.stream_timeout
837 };
838 let timeout = match streaming::timeout(req.headers(), maximum) {
839 Ok(timeout) => timeout,
840 Err(status) => return Ok(status.into_http()),
841 };
842 let deadline = tokio::time::Instant::now() + timeout;
843 let permit = match self.streams.clone().try_acquire_owned() {
844 Ok(permit) => permit,
845 Err(_) => {
846 return Ok(tonic::Status::unavailable("server at stream capacity").into_http())
847 }
848 };
849 let timer = match executor.deadline(deadline) {
850 Ok(timer) => timer,
851 Err(status) => return Ok(status.into_http()),
852 };
853 let response = if reflection {
854 let Some(state) = self.reflection.clone() else {
855 return Ok(tonic::Status::unimplemented("reflection disabled").into_http());
856 };
857 reflection::dispatch(req, state, deadline).await
858 } else {
859 let id = match self.next_stream_id.fetch_update(
860 std::sync::atomic::Ordering::Relaxed,
861 std::sync::atomic::Ordering::Relaxed,
862 |id| id.checked_add(1),
863 ) {
864 Ok(id) => id,
865 Err(_) => {
866 return Ok(tonic::Status::resource_exhausted(
867 "stream identifiers exhausted",
868 )
869 .into_http())
870 }
871 };
872 streaming::dispatch(
873 req.map(|body| {
874 body.map_err(|_| tonic::Status::internal("request transport failed"))
875 .boxed_unsync()
876 }),
877 self.clone(),
878 connection_id,
879 streaming_method.unwrap(),
880 registry,
881 deadline,
882 id,
883 )
884 .await
885 };
886 return Ok(response.map(|body| {
887 streaming::BodyGuard {
888 body,
889 _permit: permit,
890 _busy: busy,
891 _timer: timer,
892 }
893 .boxed_unsync()
894 }));
895 }
896 let _busy = busy;
897 let path = req.uri().path();
899 let (service_name, method_name) = match Self::parse_grpc_path(path) {
900 Ok((svc, method)) => (svc, method),
901 Err(e) => {
902 debug!("Invalid gRPC path: {} - {}", path, e);
903 return Ok(Self::grpc_error_response(
904 GrpcStatus::Unimplemented,
905 "path must be /package.Service/Method",
906 ));
907 }
908 };
909
910 Log::new(Some(&self.status_tx))
911 .debug(format!("gRPC request: {}/{}", service_name, method_name));
912
913 if let Some(content_type) = req.headers().get("content-type") {
915 if !content_type
916 .to_str()
917 .unwrap_or("")
918 .starts_with("application/grpc")
919 {
920 return Ok(Self::grpc_error_response(
921 GrpcStatus::Internal,
922 "expected content-type application/grpc",
923 ));
924 }
925 }
926
927 let body_bytes = match Limited::new(req.into_body(), MAX_REQUEST_BYTES)
930 .collect()
931 .await
932 {
933 Ok(collected) => collected.to_bytes(),
934 Err(e) => {
935 debug!("Failed to read gRPC request body: {}", e);
936 return Ok(Self::grpc_error_response(
937 GrpcStatus::ResourceExhausted,
938 &format!("request body rejected (limit {} bytes)", MAX_REQUEST_BYTES),
939 ));
940 }
941 };
942
943 let request_payload = match Self::decode_grpc_frame(&body_bytes) {
945 Ok(payload) => payload,
946 Err(e) => {
947 debug!("Failed to decode gRPC frame: {}", e);
948 let status = if body_bytes.first() == Some(&1) {
951 GrpcStatus::Unimplemented
952 } else {
953 GrpcStatus::Internal
954 };
955 return Ok(Self::grpc_error_response(status, "malformed gRPC frame"));
956 }
957 };
958
959 trace!("gRPC request payload: {} bytes", request_payload.len());
960
961 let response_payload = match self
963 .handle_unary(&service_name, &method_name, request_payload, connection_id)
964 .await
965 {
966 Ok(payload) => payload,
967 Err(failure) => {
968 Log::new(Some(&self.status_tx))
972 .error(format!("gRPC handler error: {}", failure.message));
973 return Ok(Self::grpc_error_response(failure.status, &failure.message));
974 }
975 };
976
977 let response_frame = Self::encode_grpc_frame(&response_payload);
979
980 let mut response = Response::new(grpc_body_with_trailers(
984 Bytes::from(response_frame),
985 GrpcStatus::Ok as i32,
986 "",
987 ));
988 *response.status_mut() = StatusCode::OK;
989 response
990 .headers_mut()
991 .insert("content-type", HeaderValue::from_static("application/grpc"));
992
993 Log::new(Some(&self.status_tx))
994 .debug(format!("gRPC response: {} bytes", response_payload.len()));
995
996 Ok(response)
997 }
998
999 fn parse_grpc_path(path: &str) -> Result<(String, String)> {
1001 if !path.starts_with('/') {
1003 bail!("Path must start with /");
1004 }
1005
1006 let parts: Vec<&str> = path[1..].split('/').collect();
1007 if parts.len() != 2 {
1008 bail!("Path must be /Service/Method");
1009 }
1010
1011 Ok((parts[0].to_string(), parts[1].to_string()))
1012 }
1013
1014 fn decode_grpc_frame(frame: &[u8]) -> Result<Vec<u8>> {
1017 if frame.len() < 5 {
1018 bail!("Frame too short (need at least 5 bytes)");
1019 }
1020
1021 let compressed = frame[0];
1022 if compressed != 0 {
1023 bail!("Compression not supported");
1024 }
1025
1026 let length = u32::from_be_bytes([frame[1], frame[2], frame[3], frame[4]]) as usize;
1027
1028 let available = frame.len() - 5;
1032 if length > available {
1033 bail!(
1034 "frame declares {} bytes but only {} follow the header",
1035 length,
1036 available
1037 );
1038 }
1039
1040 Ok(frame[5..5 + length].to_vec())
1041 }
1042
1043 fn encode_grpc_frame(payload: &[u8]) -> Vec<u8> {
1045 let length = payload.len() as u32;
1046 let mut frame = Vec::with_capacity(5 + payload.len());
1047
1048 frame.push(0);
1050
1051 frame.extend_from_slice(&length.to_be_bytes());
1053
1054 frame.extend_from_slice(payload);
1056
1057 frame
1058 }
1059
1060 fn grpc_error_response(status: GrpcStatus, message: &str) -> Response<GrpcBody> {
1079 let message = &header_safe(message);
1080 let mut res = Response::new(empty_grpc_body());
1084 *res.status_mut() = StatusCode::OK;
1085 let headers = res.headers_mut();
1086 headers.insert("content-type", HeaderValue::from_static("application/grpc"));
1087 headers.insert(
1088 "grpc-status",
1089 HeaderValue::from_str(&(status as i32).to_string())
1090 .unwrap_or_else(|_| HeaderValue::from_static("13")),
1091 );
1092 headers.insert(
1093 "grpc-message",
1094 HeaderValue::from_str(message)
1095 .unwrap_or_else(|_| HeaderValue::from_static("internal error")),
1096 );
1097 res
1098 }
1099
1100 async fn handle_unary(
1102 &self,
1103 service_name: &str,
1104 method_name: &str,
1105 request_bytes: Vec<u8>,
1106 connection_id: crate::server::connection::ConnectionId,
1107 ) -> std::result::Result<Vec<u8>, GrpcFailure> {
1108 let service_desc = self
1112 .descriptor_pool
1113 .services()
1114 .find(|s| s.full_name() == service_name)
1115 .ok_or_else(|| {
1116 GrpcFailure::new(
1117 GrpcStatus::Unimplemented,
1118 format!("unknown service {}", service_name),
1119 )
1120 })?;
1121
1122 let method_desc = service_desc
1123 .methods()
1124 .find(|m| m.name() == method_name)
1125 .ok_or_else(|| {
1126 GrpcFailure::new(
1127 GrpcStatus::Unimplemented,
1128 format!("unknown method {}/{}", service_name, method_name),
1129 )
1130 })?;
1131
1132 let input_desc = method_desc.input();
1133 let output_desc = method_desc.output();
1134
1135 Log::new(Some(&self.status_tx))
1136 .debug(format!("gRPC unary call: {}/{}", service_name, method_name));
1137
1138 let request_msg = DynamicMessage::decode(input_desc.clone(), request_bytes.as_slice())
1141 .map_err(|e| {
1142 debug!("gRPC request message did not decode: {e}");
1143 GrpcFailure::new(
1144 GrpcStatus::InvalidArgument,
1145 "could not decode request message",
1146 )
1147 })?;
1148
1149 let request_json = dynamic_message_to_json(&request_msg).map_err(|error| {
1151 debug!("gRPC request value conversion failed: {error:#}");
1152 GrpcFailure::new(
1153 grpc_status_for_value_failure(&error),
1154 "request value exceeds conversion limits or does not fit field-name JSON",
1155 )
1156 })?;
1157
1158 Log::new(Some(&self.status_tx)).trace(format!(
1159 "gRPC request JSON: {}",
1160 serde_json::to_string_pretty(&request_json)?
1161 ));
1162
1163 let response_schema = Self::build_message_schema(&output_desc);
1165
1166 let event = Event::new(
1168 &GRPC_UNARY_REQUEST_EVENT,
1169 json!({
1170 "service": service_name,
1171 "method": method_name,
1172 "request": request_json,
1173 "expected_response_schema": response_schema,
1174 }),
1175 );
1176
1177 let execution_result = call_llm(
1184 &self.llm_client,
1185 &self.app_state,
1186 self.server_id,
1187 Some(connection_id),
1188 &event,
1189 self.protocol.as_ref(),
1190 )
1191 .await
1192 .map_err(|e| {
1193 let status = grpc_status_for_llm_failure(&e);
1194 console_error!(
1195 self.status_tx,
1196 "gRPC {}/{} could not be answered: the handler failed ({}). Replying \
1197 grpc-status {} ({:?}); no response message is being invented.",
1198 service_name,
1199 method_name,
1200 e,
1201 status as i32,
1202 status
1203 );
1204 GrpcFailure::new(
1205 status,
1206 crate::utils::WireFailure::classify(&e).prefixed_text(),
1207 )
1208 })?;
1209
1210 for protocol_result in execution_result.protocol_results {
1212 match protocol_result {
1213 ActionResult::Custom { name, data } if name == "grpc_unary_response" => {
1214 let response_json = data
1216 .get("message")
1217 .context("Missing 'message' in grpc_unary_response")?;
1218
1219 let response_msg = json_to_dynamic_message(response_json, &output_desc)
1221 .map_err(|e| {
1222 debug!("gRPC handler response did not fit the schema: {e}");
1223 GrpcFailure::new(
1224 GrpcStatus::Internal,
1225 "handler's response does not fit the schema",
1226 )
1227 })?;
1228
1229 let mut response_bytes = Vec::new();
1231 response_msg
1232 .encode(&mut response_bytes)
1233 .map_err(anyhow::Error::from)?;
1234
1235 Log::new(Some(&self.status_tx))
1236 .debug(format!("gRPC response: {} bytes", response_bytes.len()));
1237
1238 return Ok(response_bytes);
1239 }
1240 ActionResult::Custom { name, data } if name == "grpc_error" => {
1241 let code = data
1246 .get("code")
1247 .and_then(|c| c.as_str())
1248 .unwrap_or("INTERNAL");
1249 let message = data
1250 .get("message")
1251 .and_then(|m| m.as_str())
1252 .unwrap_or("Internal error")
1253 .to_string();
1254
1255 let parsed = GrpcStatus::parse(code);
1263 let status = if parsed == GrpcStatus::Ok {
1264 warn!(
1265 "gRPC grpc_error asked for status OK; sending UNKNOWN instead \
1266 (decision=model_reject code_coerced): {}",
1267 message
1268 );
1269 GrpcStatus::Unknown
1270 } else {
1271 parsed
1272 };
1273 debug!("gRPC error: {} ({:?}) - {}", code, status, message);
1274 return Err(GrpcFailure::new(status, message));
1275 }
1276 _ => {
1277 }
1279 }
1280 }
1281
1282 console_error!(
1294 self.status_tx,
1295 "gRPC {}/{} could not be answered: the handler returned no usable action \
1296 (no grpc_unary_response, no grpc_error). Replying grpc-status 13 (INTERNAL); no \
1297 empty response is being invented.",
1298 service_name,
1299 method_name
1300 );
1301 Err(GrpcFailure::new(
1302 GrpcStatus::Internal,
1303 format!(
1304 "netget gRPC: handler produced no usable response for {}/{}",
1305 service_name, method_name
1306 ),
1307 ))
1308 }
1309
1310 fn build_message_schema(message_desc: &prost_reflect::MessageDescriptor) -> serde_json::Value {
1312 let mut fields = serde_json::Map::new();
1313
1314 for field in message_desc.fields() {
1315 let field_type = match field.kind() {
1316 prost_reflect::Kind::Double => "number (double)",
1317 prost_reflect::Kind::Float => "number (float)",
1318 prost_reflect::Kind::Int32
1319 | prost_reflect::Kind::Sint32
1320 | prost_reflect::Kind::Sfixed32 => "int32",
1321 prost_reflect::Kind::Int64
1322 | prost_reflect::Kind::Sint64
1323 | prost_reflect::Kind::Sfixed64 => "int64",
1324 prost_reflect::Kind::Uint32 | prost_reflect::Kind::Fixed32 => "uint32",
1325 prost_reflect::Kind::Uint64 | prost_reflect::Kind::Fixed64 => "uint64",
1326 prost_reflect::Kind::Bool => "boolean",
1327 prost_reflect::Kind::String => "string",
1328 prost_reflect::Kind::Bytes => "bytes (base64)",
1329 prost_reflect::Kind::Message(_) => "object",
1330 prost_reflect::Kind::Enum(_) => "enum (string)",
1331 };
1332
1333 let cardinality = match field.cardinality() {
1334 prost_reflect::Cardinality::Optional => "optional",
1335 prost_reflect::Cardinality::Required => "required",
1336 prost_reflect::Cardinality::Repeated => "repeated",
1337 };
1338
1339 fields.insert(
1340 field.name().to_string(),
1341 json!({
1342 "type": field_type,
1343 "cardinality": cardinality,
1344 }),
1345 );
1346 }
1347
1348 json!({
1349 "type": "object",
1350 "fields": fields,
1351 })
1352 }
1353}