Files
optimclaw/src/channels/webhook_server.rs
T
Illia PolosukhinandClaude Opus 4.6 9d156411fc Unify webhook servers into single WebhookServer
Replace the dual-server architecture (HttpChannel + WasmChannelServer both
competing for port 8080) with a single WebhookServer that composes route
fragments from all sources. Channels define routes but never spawn servers.

- Add WebhookServer struct that collects Router fragments and binds one listener
- Extract routes() from HttpChannel, remove server-spawning from start/shutdown
- Delete WasmChannelServer (keep WasmChannelRouter and route builder)
- Rewire main.rs to compose all webhook routes into one server

Co-Authored-By: Claude Opus 4.6 <[email protected]>
2026-02-06 11:12:17 -08:00

93 lines
2.8 KiB
Rust

//! Unified HTTP server for all webhook routes.
//!
//! Composes route fragments from HttpChannel, WASM channel router, etc.
//! into a single axum server. Channels define routes but never spawn servers.
use std::net::SocketAddr;
use axum::Router;
use tokio::sync::oneshot;
use tokio::task::JoinHandle;
use crate::error::ChannelError;
/// Configuration for the unified webhook server.
pub struct WebhookServerConfig {
/// Address to bind the server to.
pub addr: SocketAddr,
}
/// A single HTTP server that hosts all webhook routes.
///
/// Channels contribute route fragments via `add_routes()`, then a single
/// `start()` call binds the listener and spawns the server task.
pub struct WebhookServer {
config: WebhookServerConfig,
routes: Vec<Router>,
shutdown_tx: Option<oneshot::Sender<()>>,
handle: Option<JoinHandle<()>>,
}
impl WebhookServer {
/// Create a new webhook server with the given bind address.
pub fn new(config: WebhookServerConfig) -> Self {
Self {
config,
routes: Vec::new(),
shutdown_tx: None,
handle: None,
}
}
/// Accumulate a route fragment. Each fragment should already have its
/// state applied via `.with_state()`.
pub fn add_routes(&mut self, router: Router) {
self.routes.push(router);
}
/// Bind the listener, merge all route fragments, and spawn the server.
pub async fn start(&mut self) -> Result<(), ChannelError> {
let mut app = Router::new();
for fragment in self.routes.drain(..) {
app = app.merge(fragment);
}
let listener = tokio::net::TcpListener::bind(self.config.addr)
.await
.map_err(|e| ChannelError::StartupFailed {
name: "webhook_server".to_string(),
reason: format!("Failed to bind to {}: {}", self.config.addr, e),
})?;
tracing::info!("Webhook server listening on {}", self.config.addr);
let (shutdown_tx, shutdown_rx) = oneshot::channel();
self.shutdown_tx = Some(shutdown_tx);
let handle = tokio::spawn(async move {
if let Err(e) = axum::serve(listener, app)
.with_graceful_shutdown(async {
let _ = shutdown_rx.await;
tracing::info!("Webhook server shutting down");
})
.await
{
tracing::error!("Webhook server error: {}", e);
}
});
self.handle = Some(handle);
Ok(())
}
/// Signal graceful shutdown and wait for the server task to finish.
pub async fn shutdown(&mut self) {
if let Some(tx) = self.shutdown_tx.take() {
let _ = tx.send(());
}
if let Some(handle) = self.handle.take() {
let _ = handle.await;
}
}
}