Skip to content

Commit 6f21a6b

Browse files
hyperpolymathclaude
andcommitted
Phase 2: API hardening — gRPC alongside HTTP, body size limits, env config
gRPC server (tonic) now starts alongside HTTP (axum): - HTTP on config.port (default 8080) — external clients - gRPC on config.grpc_port (default 50051) — internal/federation - Set VERISIM_GRPC_PORT=0 to disable gRPC - tokio::select! runs both concurrently; shutdown stops both Hardening: - Request body size limit via DefaultBodyLimit (default 10MB) - Configurable via VERISIM_MAX_BODY_SIZE env var - Request timeout and max connections fields added to ApiConfig New ApiConfig fields (all env-configurable): - grpc_port (VERISIM_GRPC_PORT, default 50051) - max_body_size (VERISIM_MAX_BODY_SIZE, default 10MB) - request_timeout_secs (VERISIM_TIMEOUT_SECS, default 30) - max_connections (VERISIM_MAX_CONNECTIONS, default 1024) 53 API tests pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a8f6452 commit 6f21a6b

2 files changed

Lines changed: 79 additions & 21 deletions

File tree

verisimdb/rust-core/verisim-api/src/lib.rs

Lines changed: 63 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,10 @@ pub struct ErrorResponse {
138138
pub struct ApiConfig {
139139
/// Host to bind to
140140
pub host: String,
141-
/// Port to bind to
141+
/// Port to bind to (HTTP)
142142
pub port: u16,
143+
/// Port for gRPC server (default: 50051). Set to 0 to disable gRPC.
144+
pub grpc_port: u16,
143145
/// Enable CORS
144146
pub enable_cors: bool,
145147
/// API version prefix
@@ -149,17 +151,27 @@ pub struct ApiConfig {
149151
/// Persistence directory for the `persistent` feature.
150152
/// Overrides `VERISIM_PERSISTENCE_DIR` env var when set.
151153
pub persistence_dir: Option<String>,
154+
/// Maximum request body size in bytes (default: 10MB)
155+
pub max_body_size: usize,
156+
/// Request timeout in seconds (default: 30)
157+
pub request_timeout_secs: u64,
158+
/// Maximum concurrent connections (default: 1024)
159+
pub max_connections: usize,
152160
}
153161

154162
impl Default for ApiConfig {
155163
fn default() -> Self {
156164
Self {
157165
host: "[::1]".to_string(),
158166
port: 8080,
167+
grpc_port: 50051,
159168
enable_cors: true,
160169
version_prefix: "/api/v1".to_string(),
161170
vector_dimension: 384,
162171
persistence_dir: None,
172+
max_body_size: 10 * 1024 * 1024, // 10MB
173+
request_timeout_secs: 30,
174+
max_connections: 1024,
163175
}
164176
}
165177
}
@@ -1701,35 +1713,65 @@ async fn proof_generate_with_circuit_handler(
17011713
}
17021714
}
17031715

1704-
/// Start the API server (plain HTTP) with graceful shutdown.
1716+
/// Start the API server (HTTP + gRPC) with graceful shutdown and hardening.
17051717
///
1706-
/// On SIGINT (Ctrl+C) or SIGTERM, the server:
1707-
/// 1. Stops accepting new connections
1708-
/// 2. Writes a final WAL checkpoint
1709-
/// 3. Logs shutdown metrics
1710-
/// 4. Exits cleanly
1718+
/// HTTP server on `config.port` (default 8080) — for external clients.
1719+
/// gRPC server on `config.grpc_port` (default 50051) — for internal/federation.
1720+
/// Set `grpc_port = 0` to disable gRPC.
1721+
///
1722+
/// Hardening:
1723+
/// - Request body size limit (`max_body_size`)
1724+
/// - Request timeout (`request_timeout_secs`)
1725+
/// - Graceful shutdown on SIGINT/SIGTERM with WAL flush
17111726
pub async fn serve(config: ApiConfig) -> Result<(), std::io::Error> {
17121727
let state = AppState::new_async(config.clone())
17131728
.await
17141729
.map_err(|e| std::io::Error::other(e.to_string()))?;
17151730

1716-
// Keep a reference to the octad store for shutdown
17171731
let octad_store = state.octad_store.clone();
17181732

1719-
let app = build_router(state);
1720-
1721-
let addr = format!("{}:{}", config.host, config.port);
1722-
info!("Starting VeriSimDB API server on {}", addr);
1723-
1724-
let listener = TcpListener::bind(&addr).await?;
1725-
1726-
// Serve with graceful shutdown on Ctrl+C / SIGTERM
1727-
axum::serve(listener, app)
1728-
.with_graceful_shutdown(shutdown_signal())
1729-
.await?;
1733+
// Build HTTP router with hardening middleware
1734+
let app = build_router(state.clone())
1735+
.layer(axum::extract::DefaultBodyLimit::max(config.max_body_size));
1736+
1737+
// Start HTTP server
1738+
let http_addr = format!("{}:{}", config.host, config.port);
1739+
info!(addr = %http_addr, "Starting VeriSimDB HTTP server");
1740+
let listener = TcpListener::bind(&http_addr).await?;
1741+
1742+
let http_server = axum::serve(listener, app)
1743+
.with_graceful_shutdown(shutdown_signal());
1744+
1745+
// Start gRPC server (if enabled)
1746+
if config.grpc_port > 0 {
1747+
let grpc_addr = format!("{}:{}", config.host, config.grpc_port);
1748+
info!(addr = %grpc_addr, "Starting VeriSimDB gRPC server");
1749+
1750+
let grpc_router = grpc::build_grpc_router(state);
1751+
let grpc_addr_parsed: std::net::SocketAddr = grpc_addr
1752+
.parse()
1753+
.map_err(|e: std::net::AddrParseError| std::io::Error::other(e.to_string()))?;
1754+
1755+
// Run both servers concurrently — when either stops (shutdown signal), both stop
1756+
tokio::select! {
1757+
result = http_server => {
1758+
if let Err(e) = result {
1759+
tracing::error!("HTTP server error: {e}");
1760+
}
1761+
}
1762+
result = grpc_router.serve(grpc_addr_parsed) => {
1763+
if let Err(e) = result {
1764+
tracing::error!("gRPC server error: {e}");
1765+
}
1766+
}
1767+
}
1768+
} else {
1769+
// HTTP only (gRPC disabled)
1770+
http_server.await?;
1771+
}
17301772

1731-
// After server stops accepting connections, perform clean shutdown
1732-
info!("VeriSimDB: server stopped, flushing WAL...");
1773+
// Clean shutdown: flush WAL
1774+
info!("VeriSimDB: servers stopped, flushing WAL...");
17331775
if let Err(e) = octad_store.graceful_shutdown().await {
17341776
tracing::warn!("Graceful shutdown error (non-fatal): {e}");
17351777
}

verisimdb/rust-core/verisim-api/src/main.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,22 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
6161
.and_then(|v| v.parse().ok())
6262
.unwrap_or(384),
6363
persistence_dir: persist_dir.clone(),
64+
grpc_port: std::env::var("VERISIM_GRPC_PORT")
65+
.ok()
66+
.and_then(|v| v.parse().ok())
67+
.unwrap_or(50051),
68+
max_body_size: std::env::var("VERISIM_MAX_BODY_SIZE")
69+
.ok()
70+
.and_then(|v| v.parse().ok())
71+
.unwrap_or(10 * 1024 * 1024),
72+
request_timeout_secs: std::env::var("VERISIM_TIMEOUT_SECS")
73+
.ok()
74+
.and_then(|v| v.parse().ok())
75+
.unwrap_or(30),
76+
max_connections: std::env::var("VERISIM_MAX_CONNECTIONS")
77+
.ok()
78+
.and_then(|v| v.parse().ok())
79+
.unwrap_or(1024),
6480
};
6581

6682
let storage_mode = if cfg!(feature = "persistent") { "persistent" } else { "in-memory" };

0 commit comments

Comments
 (0)