1use std::borrow::Cow;
4use std::collections::HashSet;
5use std::convert::Infallible;
6use std::sync::{Arc, Mutex, OnceLock, PoisonError};
7
8use autumn_web::AppState;
9use autumn_web::app::AppBuilder;
10use autumn_web::plugin::Plugin;
11use autumn_web::route_listing::{RouteClassification, RouteInfo};
12use axum::response::IntoResponse;
13use tonic::body::Body as TonicBody;
14use tonic::server::NamedService;
15use tonic::service::Routes;
16use tower::{Layer, Service};
17
18use crate::config::{ConfigError, DEFAULT_SECTION, GrpcConfig, Listener, Resolved};
19use crate::error::GrpcError;
20use crate::gate::{GrpcGate, SharedOwner};
21use crate::health::GrpcHealthIndicator;
22use crate::lifecycle::LifecycleEvent;
23use crate::metrics::MetricsLayer;
24use crate::registry::{GrpcServers, ServersMetrics};
25use crate::server::{GrpcHandle, Launch, Shared};
26
27pub const PLUGIN_NAME: &str = "autumn-plugin-grpc";
29
30pub const SUPPORTED_AUTUMN_WEB: &str = "0.10";
32
33const HEALTH_SERVICE: &str = "grpc.health.v1.Health";
35const REFLECTION_V1: &str = "grpc.reflection.v1.ServerReflection";
36const REFLECTION_V1ALPHA: &str = "grpc.reflection.v1alpha.ServerReflection";
37
38type Registration = Box<dyn FnOnce(&AppState, Routes) -> Routes + Send>;
39type RouterLayer = Box<dyn FnOnce(axum::Router) -> axum::Router + Send>;
40type Override = Arc<dyn Fn(&mut GrpcConfig) + Send + Sync>;
41
42#[derive(Debug, Clone)]
44enum Posture {
45 Unclassified,
47 Public,
49 Gated(String),
51}
52
53struct Pending {
55 registrations: Vec<Registration>,
56 layers: Vec<RouterLayer>,
57 descriptors: Vec<&'static [u8]>,
58}
59
60pub struct GrpcPlugin {
91 section: String,
92 explicit: Option<Box<GrpcConfig>>,
93 overrides: Vec<Override>,
94 development: Option<bool>,
95 resolved: OnceLock<Result<Resolved, ConfigError>>,
96 registrations: Vec<Registration>,
97 service_names: Vec<&'static str>,
98 layers: Vec<RouterLayer>,
99 descriptors: Vec<&'static [u8]>,
100 posture: Posture,
101 shared: Arc<Shared>,
102 #[cfg(feature = "client")]
103 pub(crate) clients: crate::client::Registrations,
104}
105
106impl Default for GrpcPlugin {
107 fn default() -> Self {
108 Self::new()
109 }
110}
111
112impl std::fmt::Debug for GrpcPlugin {
113 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114 f.debug_struct("GrpcPlugin")
115 .field("section", &self.section)
116 .field("services", &self.service_names)
117 .finish_non_exhaustive()
118 }
119}
120
121impl GrpcPlugin {
122 #[must_use]
124 pub fn new() -> Self {
125 Self {
126 section: DEFAULT_SECTION.to_owned(),
127 explicit: None,
128 overrides: Vec::new(),
129 development: None,
130 resolved: OnceLock::new(),
131 registrations: Vec::new(),
132 service_names: Vec::new(),
133 layers: Vec::new(),
134 descriptors: Vec::new(),
135 posture: Posture::Unclassified,
136 shared: Arc::new(Shared::new()),
137 #[cfg(feature = "client")]
138 clients: crate::client::Registrations::default(),
139 }
140 }
141
142 #[must_use]
144 pub fn add_service<S>(self, service: S) -> Self
145 where
146 S: Service<http::Request<TonicBody>, Error = Infallible>
147 + NamedService
148 + Clone
149 + Send
150 + Sync
151 + 'static,
152 S::Response: IntoResponse,
153 S::Future: Send + 'static,
154 {
155 self.add_service_with(move |_| service)
156 }
157
158 #[must_use]
168 pub fn add_service_with<S, F>(mut self, build: F) -> Self
169 where
170 F: FnOnce(&AppState) -> S + Send + 'static,
171 S: Service<http::Request<TonicBody>, Error = Infallible>
172 + NamedService
173 + Clone
174 + Send
175 + Sync
176 + 'static,
177 S::Response: IntoResponse,
178 S::Future: Send + 'static,
179 {
180 self.service_names.push(S::NAME);
181 self.registrations.push(Box::new(move |state, routes| {
182 routes.add_service(build(state))
183 }));
184 self
185 }
186
187 #[must_use]
191 pub fn file_descriptor_set(mut self, encoded: &'static [u8]) -> Self {
192 self.descriptors.push(encoded);
193 self
194 }
195
196 #[must_use]
202 pub fn layer<L>(mut self, layer: L) -> Self
203 where
204 L: Layer<axum::routing::Route> + Clone + Send + Sync + 'static,
205 L::Service: Service<axum::extract::Request> + Clone + Send + Sync + 'static,
206 <L::Service as Service<axum::extract::Request>>::Response: IntoResponse + 'static,
207 <L::Service as Service<axum::extract::Request>>::Error: Into<Infallible> + 'static,
208 <L::Service as Service<axum::extract::Request>>::Future: Send + 'static,
209 {
210 self.layers
211 .push(Box::new(move |router| router.layer(layer)));
212 self
213 }
214
215 #[must_use]
226 pub fn interceptor<F>(self, interceptor: F) -> Self
227 where
228 F: tonic::service::Interceptor + Clone + Send + Sync + 'static,
229 {
230 self.layer(tonic::service::InterceptorLayer::new(interceptor))
231 }
232
233 #[must_use]
237 pub fn guard<L>(mut self, layer: L, label: impl Into<String>) -> Self
238 where
239 L: Layer<axum::routing::Route> + Clone + Send + Sync + 'static,
240 L::Service: Service<axum::extract::Request> + Clone + Send + Sync + 'static,
241 <L::Service as Service<axum::extract::Request>>::Response: IntoResponse + 'static,
242 <L::Service as Service<axum::extract::Request>>::Error: Into<Infallible> + 'static,
243 <L::Service as Service<axum::extract::Request>>::Future: Send + 'static,
244 {
245 self.posture = Posture::Gated(label.into());
246 self.layer(layer)
247 }
248
249 #[must_use]
252 pub fn guard_interceptor<F>(self, interceptor: F, label: impl Into<String>) -> Self
253 where
254 F: tonic::service::Interceptor + Clone + Send + Sync + 'static,
255 {
256 self.guard(tonic::service::InterceptorLayer::new(interceptor), label)
257 }
258
259 #[must_use]
263 pub fn public(mut self) -> Self {
264 self.posture = Posture::Public;
265 self
266 }
267
268 #[must_use]
275 pub fn route_infos(&self) -> Vec<RouteInfo> {
276 let Ok(resolved) = self.resolved() else {
277 return Vec::new();
278 };
279 let config = &resolved.config;
280 if !config.enabled {
281 return Vec::new();
282 }
283 let (classification, middleware) = match &self.posture {
284 Posture::Unclassified => (RouteClassification::Unclassified, None),
285 Posture::Public => (RouteClassification::Public, None),
286 Posture::Gated(label) => (RouteClassification::Gated, Some(label.as_str())),
287 };
288 let method = if self.section == DEFAULT_SECTION {
289 "GRPC".to_owned()
290 } else {
291 format!("GRPC:{}", self.section)
292 };
293 let mut routes: Vec<RouteInfo> = self
294 .service_names
295 .iter()
296 .map(|name| grpc_route(&method, name, classification, middleware))
297 .collect();
298 let mut framework = Vec::new();
299 if config.health {
300 framework.push(HEALTH_SERVICE);
301 }
302 if config.reflection.resolve(resolved.is_development()) {
303 framework.extend([REFLECTION_V1, REFLECTION_V1ALPHA]);
304 }
305 routes.extend(
306 framework
307 .into_iter()
308 .map(|name| grpc_route(&method, name, RouteClassification::Framework, None)),
309 );
310 routes
311 }
312
313 pub(crate) fn reset(&mut self) {
314 self.resolved = OnceLock::new();
315 }
316
317 #[must_use]
319 pub fn configure(mut self, apply: impl Fn(&mut GrpcConfig) + Send + Sync + 'static) -> Self {
320 self.overrides.push(Arc::new(apply));
321 self.reset();
322 self
323 }
324
325 #[must_use]
328 pub fn config(mut self, config: GrpcConfig) -> Self {
329 self.explicit = Some(Box::new(config));
330 self.reset();
331 self
332 }
333
334 #[must_use]
338 pub fn config_section(mut self, section: impl Into<String>) -> Self {
339 self.section = section.into();
340 self.reset();
341 self
342 }
343
344 #[must_use]
347 pub fn development(mut self, development: bool) -> Self {
348 self.development = Some(development);
349 self.reset();
350 self
351 }
352
353 #[must_use]
356 pub fn listener(self, listener: Listener) -> Self {
357 self.configure(move |c| c.listener = listener)
358 }
359
360 #[must_use]
362 pub fn bind(self, addr: impl Into<String>) -> Self {
363 let addr = addr.into();
364 self.configure(move |c| c.bind.clone_from(&addr))
365 }
366
367 #[must_use]
369 pub fn handle(&self) -> GrpcHandle {
370 GrpcHandle::new(self.shared.clone())
371 }
372
373 #[must_use]
375 pub fn service_names(&self) -> &[&'static str] {
376 &self.service_names
377 }
378
379 pub fn effective_config(&self) -> Result<&GrpcConfig, &ConfigError> {
385 self.resolved().as_ref().map(|r| &r.config)
386 }
387
388 fn resolved(&self) -> &Result<Resolved, ConfigError> {
389 self.resolved.get_or_init(|| self.resolve(None))
390 }
391
392 fn resolve(&self, env: Option<&dyn autumn_web::config::Env>) -> Result<Resolved, ConfigError> {
394 #[cfg(feature = "client")]
395 let clients = self.clients.names();
396 #[cfg(not(feature = "client"))]
397 let clients: Vec<&str> = Vec::new();
398 let mut resolved = match (&self.explicit, env) {
399 (Some(config), _) => Resolved::explicit((**config).clone()),
400 (None, Some(env)) => {
401 GrpcConfig::resolve_with_env_and_clients(&self.section, env, &clients)?
402 }
403 (None, None) => GrpcConfig::resolve_with_clients(&self.section, &clients)?,
404 };
405 for apply in &self.overrides {
406 apply(&mut resolved.config);
407 }
408 if let Some(development) = self.development {
409 if development { "dev" } else { "prod" }.clone_into(&mut resolved.profile);
410 }
411 resolved.config.validate()?;
412 Ok(resolved)
413 }
414}
415
416fn grpc_route(
417 method: &str,
418 service: &str,
419 classification: RouteClassification,
420 middleware: Option<&str>,
421) -> RouteInfo {
422 RouteInfo {
423 method: method.to_owned(),
424 path: format!("/{service}/*"),
425 handler: format!("autumn_plugin_grpc::{service}"),
426 classification,
427 middleware: middleware.map(str::to_owned).into_iter().collect(),
428 ..RouteInfo::default()
429 }
430}
431
432const PLUGIN_SERVICE_MAX_MESSAGE: usize = 16 * 1024;
435
436fn check_unique(
438 user_names: &[&'static str],
439 config: &GrpcConfig,
440 development: bool,
441) -> Result<(), GrpcError> {
442 let mut taken: HashSet<&str> = HashSet::new();
443 if config.health {
444 taken.insert(HEALTH_SERVICE);
445 }
446 if config.reflection.resolve(development) {
447 taken.extend([REFLECTION_V1, REFLECTION_V1ALPHA]);
448 }
449 for name in user_names {
450 if !taken.insert(name) {
451 return Err(GrpcError::DuplicateService((*name).to_owned()));
452 }
453 }
454 Ok(())
455}
456
457fn user_routes(
461 state: &AppState,
462 registrations: Vec<Registration>,
463 layers: Vec<RouterLayer>,
464) -> Routes {
465 let registered = registrations
466 .into_iter()
467 .fold(Routes::default(), |routes, register| {
468 register(state, routes)
469 });
470 let layered = layers
471 .into_iter()
472 .fold(registered.into_axum_router(), |router, apply| apply(router));
473 Routes::from(layered)
474}
475
476fn add_reflection(
478 routes: Routes,
479 descriptors: &[&'static [u8]],
480 health: bool,
481) -> Result<Routes, GrpcError> {
482 let configure = || {
483 let mut builder = tonic_reflection::server::Builder::configure();
484 if health {
485 builder =
486 builder.register_encoded_file_descriptor_set(tonic_health::pb::FILE_DESCRIPTOR_SET);
487 }
488 descriptors.iter().fold(builder, |builder, set| {
489 builder.register_encoded_file_descriptor_set(set)
490 })
491 };
492 let invalid = |e: tonic_reflection::server::Error| GrpcError::Reflection(e.to_string());
493 let v1 = configure()
494 .build_v1()
495 .map_err(invalid)?
496 .max_decoding_message_size(PLUGIN_SERVICE_MAX_MESSAGE);
497 let v1alpha = configure()
498 .build_v1alpha()
499 .map_err(invalid)?
500 .max_decoding_message_size(PLUGIN_SERVICE_MAX_MESSAGE);
501 Ok(routes.add_service(v1).add_service(v1alpha))
502}
503
504fn build_routes(
507 state: &AppState,
508 pending: Pending,
509 config: &GrpcConfig,
510 development: bool,
511 shared: &Arc<Shared>,
512 user_names: &[&'static str],
513) -> Result<(Routes, Option<tonic_health::server::HealthReporter>), GrpcError> {
514 let Pending {
515 registrations,
516 layers,
517 descriptors,
518 } = pending;
519 check_unique(user_names, config, development)?;
520 let mut routes = user_routes(state, registrations, layers);
521 let mut known: HashSet<String> = user_names.iter().map(|n| (*n).to_owned()).collect();
522
523 let reporter = config.health.then(|| {
524 let (reporter, service) = tonic_health::server::health_reporter();
525 let service = service.max_decoding_message_size(PLUGIN_SERVICE_MAX_MESSAGE);
526 routes = std::mem::take(&mut routes).add_service(service);
527 known.insert(HEALTH_SERVICE.to_owned());
528 reporter
529 });
530 if config.reflection.resolve(development) {
531 routes = add_reflection(routes, &descriptors, config.health)?;
532 known.insert(REFLECTION_V1.to_owned());
533 known.insert(REFLECTION_V1ALPHA.to_owned());
534 }
535
536 let mut sets = descriptors.clone();
537 if config.health {
538 sets.push(tonic_health::pb::FILE_DESCRIPTOR_SET);
539 }
540 let mut methods = crate::metrics::methods_from_descriptors(&sets);
541 for name in [REFLECTION_V1, REFLECTION_V1ALPHA] {
543 methods.insert(
544 name.to_owned(),
545 HashSet::from(["ServerReflectionInfo".to_owned()]),
546 );
547 }
548 shared
549 .metrics
550 .configure(config.max_metric_series, known, methods);
551 let with_state = routes
552 .into_axum_router()
553 .layer(axum::Extension(state.clone()));
554 let observed = if config.metrics {
555 with_state.layer(MetricsLayer::new(shared.metrics.clone()))
556 } else {
557 with_state
558 };
559 Ok((Routes::from(observed), reporter))
560}
561
562impl Plugin for GrpcPlugin {
563 fn name(&self) -> Cow<'static, str> {
565 Cow::Owned(format!("{PLUGIN_NAME}@{}", self.section))
566 }
567
568 fn build(mut self, app: AppBuilder) -> AppBuilder {
569 let app = app.config_section(self.section.clone());
570 let (config, development, problem) = match self.resolved() {
571 Ok(resolved) => (resolved.config.clone(), resolved.is_development(), None),
572 Err(error) => (GrpcConfig::default(), false, Some(error.clone())),
573 };
574 let shared = self.shared.clone();
575
576 if let Some(error) = problem {
577 return fail_at_startup(app, shared, &GrpcError::Config(error));
578 }
579 #[cfg(feature = "client")]
581 let app = {
582 let registrations = std::mem::take(&mut self.clients);
583 match crate::client::prepare(&app, registrations, &config, &self.section) {
584 Ok(Some(prepared)) => prepared.install(app),
585 Ok(None) => app,
586 Err(error) => return fail_at_startup(app, shared, &error),
587 }
588 };
589 if !config.enabled {
590 tracing::info!(section = %self.section, "gRPC server disabled by configuration");
591 return app;
592 }
593 let app = if config.listener == Listener::Shared {
594 if let Some(owner) = app.extension::<SharedOwner>() {
595 let error = GrpcError::SharedListenerTaken(owner.0.clone());
596 return fail_at_startup(app, shared, &error);
597 }
598 app.with_extension(SharedOwner(self.section.clone()))
599 .static_gate(GrpcGate::new(shared.clone()))
600 } else {
601 app
602 };
603
604 let pending = Arc::new(Mutex::new(Some(Pending {
605 registrations: std::mem::take(&mut self.registrations),
606 layers: std::mem::take(&mut self.layers),
607 descriptors: std::mem::take(&mut self.descriptors),
608 })));
609 let user_names = self.service_names.clone();
610 let section = self.section.clone();
611 let declared = self.route_infos();
612 let existing = app.extension::<GrpcServers>().cloned();
613 let first_server = existing.is_none();
614 let servers = existing.unwrap_or_default();
615 let app = if first_server {
616 app.with_extension(servers.clone())
617 } else {
618 app
619 };
620 servers.insert(section.clone(), GrpcHandle::new(shared.clone()));
621 let hook_servers = servers.clone();
622 let is_default = section == DEFAULT_SECTION;
623 let hook_shared = shared.clone();
624 let app = app.on_startup(move |state| {
625 let pending = pending.clone();
626 let shared = hook_shared.clone();
627 let config = config.clone();
628 let user_names = user_names.clone();
629 let servers = hook_servers.clone();
630 async move {
631 let taken = pending
632 .lock()
633 .unwrap_or_else(PoisonError::into_inner)
634 .take();
635 let result = match taken {
636 Some(pending) => {
637 launch(&state, pending, config, development, &shared, &user_names).await
638 }
639 None => Err(GrpcError::AlreadyStarted),
640 };
641 match result {
642 Ok(listening) => {
643 tracing::info!(%listening, services = ?user_names, "gRPC server listening");
644 state.insert_extension(servers);
645 if is_default {
646 state.insert_extension(GrpcHandle::new(shared));
647 }
648 Ok(())
649 }
650 Err(error) => {
651 let _ = shared.lifecycle.apply(LifecycleEvent::StartFailed);
652 shared.mark_stopped();
653 Err(startup_error(&error))
654 }
655 }
656 }
657 });
658
659 let app = if first_server {
662 app.metrics_source(PLUGIN_NAME, Arc::new(ServersMetrics(servers)))
663 } else {
664 app
665 };
666 let stop_shared = shared.clone();
667 app.declare_plugin_routes(declared)
668 .health_indicator(section, Arc::new(GrpcHealthIndicator { shared }))
669 .on_shutdown(move || {
670 let handle = GrpcHandle::new(stop_shared.clone());
671 async move { handle.shutdown().await }
672 })
673 }
674}
675
676fn fail_at_startup(app: AppBuilder, shared: Arc<Shared>, error: &GrpcError) -> AppBuilder {
678 let message = error.to_string();
680 app.on_startup(move |_state| {
681 let shared = shared.clone();
682 let message = message.clone();
683 async move {
684 let _ = shared.lifecycle.apply(LifecycleEvent::StartFailed);
685 shared.mark_stopped();
686 Err(startup_message(&message))
687 }
688 })
689}
690
691fn fit_grace_to_autumn(config: &mut GrpcConfig, state: &AppState) {
695 let budget_ms = state
696 .config_arc()
697 .server
698 .shutdown_timeout_secs
699 .saturating_mul(1000);
700 let kill_ms = u64::try_from(crate::server::KILL_WAIT.as_millis()).unwrap_or(u64::MAX);
702 let limit = budget_ms.saturating_sub(kill_ms.min(budget_ms / 2)).max(1);
703 if config.shutdown_grace_ms > limit {
704 tracing::warn!(
705 shutdown_grace_ms = config.shutdown_grace_ms,
706 limit_ms = limit,
707 "gRPC shutdown_grace_ms does not fit in server.shutdown_timeout_secs; using the limit"
708 );
709 config.shutdown_grace_ms = limit;
710 }
711}
712
713fn warn_on_open_reflection(config: &GrpcConfig, development: bool) {
716 let open = config
717 .bind_addr(development)
718 .is_ok_and(|addr| !addr.ip().is_loopback());
719 if open && config.reflection.resolve(development) {
720 tracing::warn!(
721 bind = %config.bind,
722 "gRPC reflection is on and the listener is not on loopback; set `reflection = false` to hide the API"
723 );
724 }
725}
726
727enum Listening {
729 Dedicated(std::net::SocketAddr),
730 Shared,
731}
732
733impl std::fmt::Display for Listening {
734 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
735 match self {
736 Self::Dedicated(addr) => write!(f, "{addr}"),
737 Self::Shared => f.write_str("Autumn's HTTP port (shared)"),
738 }
739 }
740}
741
742async fn launch(
743 state: &AppState,
744 pending: Pending,
745 config: GrpcConfig,
746 development: bool,
747 shared: &Arc<Shared>,
748 user_names: &[&'static str],
749) -> Result<Listening, GrpcError> {
750 if config.listener == Listener::Shared {
751 launch_shared(state, pending, config, development, shared, user_names).await?;
752 return Ok(Listening::Shared);
753 }
754 launch_dedicated(state, pending, config, development, shared, user_names)
755 .await
756 .map(Listening::Dedicated)
757}
758
759fn check_shared(state: &AppState, config: &GrpcConfig, development: bool) -> Result<(), GrpcError> {
761 let autumn = state.config_arc();
762 if autumn.server.tls.is_some() {
763 return Err(GrpcError::SharedListenerNeedsH2(SUPPORTED_AUTUMN_WEB));
764 }
765 for warning in shared_warnings(config, &autumn.server.host, development) {
766 tracing::warn!("{warning}");
767 }
768 Ok(())
769}
770
771fn shared_warnings(config: &GrpcConfig, autumn_host: &str, development: bool) -> Vec<String> {
774 let mut warnings = Vec::new();
775 let ignored = config.dedicated_only_settings();
776 if !ignored.is_empty() {
777 warnings.push(format!(
778 "these gRPC settings have no effect with `listener = \"shared\"`, because Autumn's server owns the port: {}",
779 ignored.join(", ")
780 ));
781 }
782 let host = autumn_host.trim();
783 let loopback = host.eq_ignore_ascii_case("localhost")
784 || host
785 .parse::<std::net::IpAddr>()
786 .is_ok_and(|ip| ip.is_loopback());
787 if !loopback && config.reflection.resolve(development) {
788 warnings.push(format!(
789 "gRPC reflection is on and Autumn's port is not on loopback (host `{host}`); set `reflection = false` to hide the API"
790 ));
791 }
792 warnings
793}
794
795async fn launch_shared(
796 state: &AppState,
797 pending: Pending,
798 mut config: GrpcConfig,
799 development: bool,
800 shared: &Arc<Shared>,
801 user_names: &[&'static str],
802) -> Result<(), GrpcError> {
803 check_shared(state, &config, development)?;
804 fit_grace_to_autumn(&mut config, state);
805 let (routes, health) = build_routes(state, pending, &config, development, shared, user_names)?;
806 let health_names = user_names.iter().map(|n| (*n).to_owned()).collect();
807 crate::server::start_shared(
808 shared,
809 Launch {
810 config,
811 development,
812 routes,
813 health,
814 health_names,
815 #[cfg(feature = "tls")]
816 tls: None,
817 },
818 )
819 .await?;
820 crate::server::watch_readiness(shared, state.clone());
821 #[cfg(feature = "multiplex")]
822 crate::server::drain_on_autumn_shutdown(shared, state);
823 Ok(())
824}
825
826async fn launch_dedicated(
827 state: &AppState,
828 pending: Pending,
829 mut config: GrpcConfig,
830 development: bool,
831 shared: &Arc<Shared>,
832 user_names: &[&'static str],
833) -> Result<std::net::SocketAddr, GrpcError> {
834 #[cfg(feature = "tls")]
836 let tls = crate::tls::server_config(&config.tls)?;
837 #[cfg(not(feature = "tls"))]
838 crate::tls::server_config(&config.tls)?;
839
840 fit_grace_to_autumn(&mut config, state);
841 warn_on_open_reflection(&config, development);
842 let (routes, health) = build_routes(state, pending, &config, development, shared, user_names)?;
843 let health_names = user_names.iter().map(|n| (*n).to_owned()).collect();
844 let addr = crate::server::start(
845 shared,
846 Launch {
847 config,
848 development,
849 routes,
850 health,
851 health_names,
852 #[cfg(feature = "tls")]
853 tls,
854 },
855 )
856 .await?;
857 crate::server::watch_readiness(shared, state.clone());
858 Ok(addr)
859}
860
861pub fn startup_error(error: &GrpcError) -> autumn_web::AutumnError {
862 startup_message(&error.to_string())
863}
864
865fn startup_message(message: &str) -> autumn_web::AutumnError {
866 autumn_web::AutumnError::internal_server_error_msg(format!("{PLUGIN_NAME}: {message}"))
867}
868
869#[cfg(test)]
870mod tests {
871 use super::*;
872 use crate::config::Toggle;
873
874 #[cfg(feature = "client")]
875 #[test]
876 fn a_registered_client_reads_env_overrides_and_resets_the_cache() {
877 use autumn_web::config::MockEnv;
878
879 let dir =
880 std::env::temp_dir().join(format!("autumn-grpc-plugin-env-{}", std::process::id()));
881 let _ = std::fs::create_dir_all(&dir);
882 let env = MockEnv::new()
883 .with("AUTUMN_MANIFEST_DIR", dir.to_str().unwrap_or_default())
884 .with(
885 "AUTUMN_GRPC__CLIENTS__LEDGER__ENDPOINT",
886 "http://ledger:7000",
887 )
888 .with("AUTUMN_GRPC__CLIENTS__LEDGER__TIMEOUT_MS", "250");
889 let plugin = GrpcPlugin::new();
890 let before = plugin.resolve(Some(&env)).map(|r| r.config);
891 assert!(
892 before.is_ok_and(|c| c.clients.is_empty()),
893 "no name, no leaf"
894 );
895
896 let _ = plugin.effective_config();
898 let plugin = plugin.client("ledger", |channel| channel);
899 assert!(plugin.resolved.get().is_none(), "client() resets the cache");
900 let after = plugin.resolve(Some(&env)).map(|r| r.config);
901 let ledger = after.as_ref().map(|c| c.clients.get("ledger").cloned());
902 assert!(
903 matches!(&ledger, Ok(Some(l)) if l.endpoint == "http://ledger:7000" && l.timeout_ms == 250),
904 "{ledger:?}"
905 );
906 }
907
908 #[test]
909 fn shared_mode_warns_once_for_ignored_settings() {
910 let mut config = GrpcConfig {
911 reflection: Toggle::Off,
912 ..GrpcConfig::default()
913 };
914 assert_eq!(
915 shared_warnings(&config, "0.0.0.0", false),
916 Vec::<String>::new()
917 );
918
919 config.bind = "127.0.0.1:0".to_owned();
920 config.max_connection_age_ms = 5;
921 let warnings = shared_warnings(&config, "0.0.0.0", false);
922 assert_eq!(warnings.len(), 1, "{warnings:?}");
923 assert!(warnings[0].contains("bind, max_connection_age_ms"));
924 }
925
926 #[test]
927 fn shared_mode_warns_for_reflection_off_loopback() {
928 let mut config = GrpcConfig {
929 reflection: Toggle::On,
930 ..GrpcConfig::default()
931 };
932 for host in ["127.0.0.1", "::1", "localhost", " LocalHost "] {
933 assert!(shared_warnings(&config, host, false).is_empty(), "{host}");
934 }
935 for host in ["0.0.0.0", "10.0.0.5", "example.com"] {
936 let warnings = shared_warnings(&config, host, false);
937 assert_eq!(warnings.len(), 1, "{host}");
938 assert!(warnings[0].contains("reflection"));
939 }
940 config.reflection = Toggle::Auto;
941 assert_eq!(shared_warnings(&config, "0.0.0.0", true).len(), 1, "dev");
942 assert!(
943 shared_warnings(&config, "0.0.0.0", false).is_empty(),
944 "prod"
945 );
946 }
947}