use super::allowlist::{AllowedHost, AllowedOrigin, HttpPath};
use super::lease::{LeaseWindow, ListenLease};
use super::session_manager::IdleTimeout;
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct HttpConfig {
pub bind: std::net::SocketAddr,
pub path: HttpPath,
pub max_request_body: RequestBodyLimit,
pub max_concurrent_sessions: SessionLimit,
pub session_idle_timeout: IdleTimeout,
pub header_read_timeout: HeaderReadTimeout,
pub max_concurrent_connections: ConnectionLimit,
pub stream_liveness: StreamLiveness,
pub listen_lease: Option<ListenLease>,
pub allowed_origins: Box<[AllowedOrigin]>,
pub allowed_hosts: Box<[AllowedHost]>,
pub response_stream_deadline: ResponseStreamDeadline,
pub write_stall_timeout: WriteStallTimeout,
}
impl HttpConfig {
#[must_use]
pub fn new(bind: std::net::SocketAddr) -> Self {
Self {
bind,
path: HttpPath::default(),
max_request_body: RequestBodyLimit::DEFAULT,
max_concurrent_sessions: SessionLimit::DEFAULT,
session_idle_timeout: IdleTimeout::DEFAULT,
header_read_timeout: HeaderReadTimeout::DEFAULT,
max_concurrent_connections: ConnectionLimit::DEFAULT,
stream_liveness: StreamLiveness::DEFAULT,
listen_lease: None,
allowed_origins: Box::default(),
allowed_hosts: Box::default(),
response_stream_deadline: ResponseStreamDeadline::DEFAULT,
write_stall_timeout: WriteStallTimeout::DEFAULT,
}
}
#[must_use]
pub fn with_path(mut self, path: HttpPath) -> Self {
self.path = path;
self
}
#[must_use]
pub const fn with_max_request_body(mut self, limit: RequestBodyLimit) -> Self {
self.max_request_body = limit;
self
}
#[must_use]
pub const fn with_max_concurrent_sessions(mut self, max: SessionLimit) -> Self {
self.max_concurrent_sessions = max;
self
}
#[must_use]
pub const fn with_header_read_timeout(mut self, timeout: HeaderReadTimeout) -> Self {
self.header_read_timeout = timeout;
self
}
#[must_use]
pub const fn with_max_concurrent_connections(mut self, max: ConnectionLimit) -> Self {
self.max_concurrent_connections = max;
self
}
#[must_use]
pub fn with_allowed_origins(
mut self,
origins: impl IntoIterator<Item = AllowedOrigin>,
) -> Self {
self.allowed_origins = origins.into_iter().collect();
self
}
#[must_use]
pub fn with_allowed_hosts(mut self, hosts: impl IntoIterator<Item = AllowedHost>) -> Self {
self.allowed_hosts = hosts.into_iter().collect();
self
}
#[must_use]
pub const fn with_write_stall_timeout(mut self, timeout: WriteStallTimeout) -> Self {
self.write_stall_timeout = timeout;
self
}
#[must_use]
pub const fn with_response_stream_deadline(mut self, deadline: ResponseStreamDeadline) -> Self {
self.response_stream_deadline = deadline;
self
}
#[must_use]
pub const fn with_session_idle_timeout(mut self, timeout: IdleTimeout) -> Self {
self.session_idle_timeout = timeout;
self
}
#[must_use]
pub const fn with_stream_liveness(mut self, liveness: StreamLiveness) -> Self {
self.stream_liveness = liveness;
self
}
#[must_use]
pub const fn with_listen_lease(mut self, lease: ListenLease) -> Self {
self.listen_lease = Some(lease);
self
}
#[must_use]
pub const fn effective_listen_lease(&self) -> ListenLease {
match (self.listen_lease, self.stream_liveness) {
(Some(lease), _) => lease,
(None, StreamLiveness::Probe { .. }) => ListenLease::Renew(LeaseWindow::DEFAULT),
(None, StreamLiveness::Disabled) => ListenLease::Unbounded,
}
}
}
macro_rules! non_zero_duration {
($(#[$meta:meta])* $vis:vis $name:ident, $default:expr) => {
$(#[$meta])*
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
$vis struct $name(std::time::Duration);
impl $name {
$vis const DEFAULT: Self = match Self::new($default) {
Some(duration) => duration,
None => panic!("the default duration must be non-zero"),
};
#[must_use]
$vis const fn new(duration: std::time::Duration) -> Option<Self> {
if duration.is_zero() {
None
} else {
Some(Self(duration))
}
}
#[must_use]
$vis const fn get(self) -> std::time::Duration {
self.0
}
}
};
}
pub(super) use non_zero_duration;
macro_rules! non_zero_limit {
(
$(#[$meta:meta])* $name:ident,
$default:expr, $default_doc:literal,
$max:expr, $max_doc:literal
) => {
$(#[$meta])*
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct $name(usize);
const _: () = assert!($default <= $max, "the default limit must not exceed the maximum");
impl $name {
#[doc = $default_doc]
pub const DEFAULT: Self = Self($default);
#[doc = concat!("`None` for zero; larger values are clamped to ", $max_doc, ".")]
#[must_use]
pub fn new(max: usize) -> Option<Self> {
(max > 0).then(|| Self(max.min($max)))
}
#[doc = concat!("The wrapped limit, between 1 and ", $max_doc, ".")]
#[must_use]
pub const fn get(self) -> usize {
self.0
}
}
};
}
non_zero_duration! {
pub ProbeInterval, std::time::Duration::from_mins(1)
}
non_zero_duration! {
pub ProbeDeadline, std::time::Duration::from_secs(30)
}
non_zero_duration! {
pub ResponseStreamDeadline, std::time::Duration::from_hours(1)
}
const _: () = assert!(
ResponseStreamDeadline::DEFAULT.get().as_secs()
>= 2 * crate::config::MAX_TIMEOUT_SECONDS
+ crate::bridge::INDEXING_STALENESS_BOUND.as_secs()
+ 300,
"the default response stream deadline must outlast the longest single tool call"
);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StreamLiveness {
Disabled,
Probe {
interval: ProbeInterval,
deadline: ProbeDeadline,
},
}
impl StreamLiveness {
pub const DEFAULT: Self = Self::Probe {
interval: ProbeInterval::DEFAULT,
deadline: ProbeDeadline::DEFAULT,
};
}
impl Default for StreamLiveness {
fn default() -> Self {
Self::DEFAULT
}
}
non_zero_duration! {
pub HeaderReadTimeout, std::time::Duration::from_secs(30)
}
non_zero_duration! {
pub WriteStallTimeout, std::time::Duration::from_secs(30)
}
non_zero_limit! {
ConnectionLimit,
512,
"512: room for 100 sessions each holding a GET stream, a `subscriptions/listen` stream and one in-flight POST.",
tokio::sync::Semaphore::MAX_PERMITS,
"the most a [`tokio::sync::Semaphore`] supports"
}
non_zero_limit! {
SessionLimit,
100,
"100 concurrent sessions.",
tokio::sync::Semaphore::MAX_PERMITS,
"the most a [`tokio::sync::Semaphore`] supports"
}
non_zero_limit! {
RequestBodyLimit,
4 * 1024 * 1024,
"4 MiB, matching `rmcp`'s own default.",
64 * 1024 * 1024,
"64 MiB"
}
#[cfg(test)]
mod tests {
use std::net::SocketAddr;
use super::*;
use crate::transport::test_support::short_lease;
#[test]
fn test_http_config_fields() {
let addr: SocketAddr = "127.0.0.1:3000".parse().unwrap();
let cfg = HttpConfig::new(addr);
assert_eq!(cfg.bind, addr);
assert_eq!(cfg.path, crate::HttpPath::default());
}
#[test]
fn test_http_config_clone() {
let cfg =
HttpConfig::new("127.0.0.1:3001".parse().unwrap()).with_path("/test".parse().unwrap());
let cloned = cfg.clone();
assert_eq!(cloned.bind, cfg.bind);
assert_eq!(cloned.path, cfg.path);
}
#[test]
fn test_http_config_new_uses_default_limits() {
let cfg = HttpConfig::new("127.0.0.1:3003".parse().unwrap());
assert_eq!(cfg.max_request_body, crate::RequestBodyLimit::DEFAULT);
assert_eq!(cfg.max_concurrent_sessions, crate::SessionLimit::DEFAULT);
}
#[test]
fn test_http_config_with_max_request_body_overrides_default() {
let cfg = HttpConfig::new("127.0.0.1:3004".parse().unwrap())
.with_max_request_body(crate::RequestBodyLimit::new(1024).unwrap());
assert_eq!(cfg.max_request_body.get(), 1024);
assert_eq!(cfg.max_concurrent_sessions, crate::SessionLimit::DEFAULT);
}
#[test]
fn test_http_config_with_max_concurrent_sessions_overrides_default() {
let cfg = HttpConfig::new("127.0.0.1:3005".parse().unwrap())
.with_max_concurrent_sessions(crate::SessionLimit::new(5).unwrap());
assert_eq!(cfg.max_concurrent_sessions.get(), 5);
assert_eq!(cfg.max_request_body, crate::RequestBodyLimit::DEFAULT);
}
#[test]
fn test_connection_settings_reject_zero_and_clamp_to_the_semaphore_maximum() {
assert_eq!(HeaderReadTimeout::new(std::time::Duration::ZERO), None);
assert_eq!(ConnectionLimit::new(0), None);
assert_eq!(
ConnectionLimit::new(usize::MAX).unwrap().get(),
tokio::sync::Semaphore::MAX_PERMITS
);
let semaphore =
tokio::sync::Semaphore::new(ConnectionLimit::new(usize::MAX).unwrap().get());
assert_eq!(
semaphore.available_permits(),
tokio::sync::Semaphore::MAX_PERMITS
);
}
#[test]
fn test_probe_settings_reject_zero() {
assert_eq!(ProbeInterval::new(std::time::Duration::ZERO), None);
assert_eq!(ProbeDeadline::new(std::time::Duration::ZERO), None);
assert!(ProbeInterval::new(std::time::Duration::from_nanos(1)).is_some());
assert!(ProbeDeadline::new(std::time::Duration::from_nanos(1)).is_some());
}
#[test]
fn test_http_config_stream_liveness_defaults_to_probe_and_is_overridable() {
let cfg = HttpConfig::new("127.0.0.1:3003".parse().unwrap());
assert_eq!(cfg.stream_liveness, StreamLiveness::DEFAULT);
let cfg = cfg.with_stream_liveness(StreamLiveness::Disabled);
assert_eq!(cfg.stream_liveness, StreamLiveness::Disabled);
}
#[test]
fn test_listen_lease_follows_stream_liveness_unless_overridden() {
use crate::transport::{LeaseWindow, ListenLease};
let cfg = HttpConfig::new("127.0.0.1:3000".parse().unwrap());
assert_eq!(
cfg.effective_listen_lease(),
ListenLease::Renew(LeaseWindow::DEFAULT)
);
let off = cfg.with_stream_liveness(StreamLiveness::Disabled);
assert_eq!(off.effective_listen_lease(), ListenLease::Unbounded);
let forced = off.with_listen_lease(short_lease(5));
assert_eq!(forced.effective_listen_lease(), short_lease(5));
}
}