use std::time::Duration;
use axum::Json;
use axum::extract::{MatchedPath, Request};
use axum::http::StatusCode;
use axum::middleware::Next;
use axum::response::{IntoResponse, Response};
use super::coordinator_routes::COORDINATOR_CHAT_PATH;
pub(crate) const STANDARD_DEADLINE: Duration = Duration::from_secs(25);
pub(crate) const LIFECYCLE_DEADLINE: Duration = Duration::from_secs(90);
pub(crate) const DOCTOR_DEADLINE: Duration = Duration::from_secs(115);
pub(crate) const LLM_DEADLINE: Duration = Duration::from_secs(125);
pub(crate) const PROVISION_DEADLINE: Duration = Duration::from_secs(175);
pub(crate) const SURVEY_DEADLINE: Duration = Duration::from_secs(1795);
pub(crate) type LongRoute = (&'static str, &'static str, Duration);
pub(crate) const LONG_ROUTES: &[LongRoute] = &[
(
"POST",
"/api/v1/sessions/managed/prune-worktrees",
SURVEY_DEADLINE,
),
(
"GET",
"/api/v1/sessions/managed/reconcile-worktrees",
SURVEY_DEADLINE,
),
("POST", "/api/v1/sessions/managed", PROVISION_DEADLINE),
("POST", "/api/v1/control/sessions/run", PROVISION_DEADLINE),
("POST", "/api/v1/manager/act", PROVISION_DEADLINE),
("POST", "/rpc", PROVISION_DEADLINE),
("POST", "/llm/chat", LLM_DEADLINE),
("POST", COORDINATOR_CHAT_PATH, LLM_DEADLINE),
("POST", "/api/v1/session-manager/chat", LLM_DEADLINE),
("POST", "/api/v1/manager/chat", LLM_DEADLINE),
("GET", "/api/v1/manager/digest", LLM_DEADLINE),
("POST", "/api/v1/manager/route-task", LLM_DEADLINE),
("POST", "/api/v1/sessions/proxy/message", LLM_DEADLINE),
(
"GET",
"/api/v1/sessions/proxy/summary/{conversation_key}",
LLM_DEADLINE,
),
("GET", "/api/v1/doctor", DOCTOR_DEADLINE),
(
"DELETE",
"/api/v1/sessions/managed/{id}",
LIFECYCLE_DEADLINE,
),
(
"POST",
"/api/v1/sessions/managed/{id}/runtime-stop",
LIFECYCLE_DEADLINE,
),
(
"POST",
"/api/v1/sessions/managed/{id}/resume",
LIFECYCLE_DEADLINE,
),
(
"POST",
"/api/v1/sessions/managed/{id}/reactivate",
LIFECYCLE_DEADLINE,
),
(
"POST",
"/api/v1/sessions/managed/{id}/decommission",
LIFECYCLE_DEADLINE,
),
(
"POST",
"/api/v1/sessions/managed/{id}/delete",
LIFECYCLE_DEADLINE,
),
(
"POST",
"/api/v1/sessions/managed/{id}/sync-assets",
LIFECYCLE_DEADLINE,
),
(
"POST",
"/api/v1/sessions/managed/sync-assets",
LIFECYCLE_DEADLINE,
),
("POST", "/api/v1/sessions/managed/adopt", LIFECYCLE_DEADLINE),
(
"POST",
"/api/v1/sessions/managed/adopt-worktree",
LIFECYCLE_DEADLINE,
),
("POST", "/api/v1/sessions/managed/prune", LIFECYCLE_DEADLINE),
(
"POST",
"/api/v1/sessions/managed/decommission-ephemeral",
LIFECYCLE_DEADLINE,
),
(
"POST",
"/api/v1/control/sessions/{id}/connect",
LIFECYCLE_DEADLINE,
),
(
"POST",
"/api/v1/control/sessions/{id}/stop",
LIFECYCLE_DEADLINE,
),
("DELETE", "/sessions/{id}", LIFECYCLE_DEADLINE),
("DELETE", "/sessions/dead", LIFECYCLE_DEADLINE),
("POST", "/sessions/discover", LIFECYCLE_DEADLINE),
("POST", "/sessions/{id}/pause", LIFECYCLE_DEADLINE),
("POST", "/sessions/{id}/resume", LIFECYCLE_DEADLINE),
("POST", "/tmux/adopt", LIFECYCLE_DEADLINE),
("POST", "/claude-config/apply", LIFECYCLE_DEADLINE),
("POST", "/claude-config/restart", LIFECYCLE_DEADLINE),
("POST", "/claude-config/deploy", LIFECYCLE_DEADLINE),
("POST", "/claude-config/restore", LIFECYCLE_DEADLINE),
("POST", "/api/v1/report-bug", LIFECYCLE_DEADLINE),
(
"POST",
"/api/v1/delegations/{agent_id}/repair",
LIFECYCLE_DEADLINE,
),
(
"POST",
"/api/v1/delegations/by-id/{delegation_id}/repair",
LIFECYCLE_DEADLINE,
),
("GET", "/projects/discover", LIFECYCLE_DEADLINE),
("POST", "/api/v1/projects", LIFECYCLE_DEADLINE),
];
pub(crate) fn deadline_for(method: &str, template: Option<&str>) -> Duration {
let Some(template) = template else {
return STANDARD_DEADLINE;
};
LONG_ROUTES
.iter()
.find(|(m, t, _)| *m == method && *t == template)
.map_or(STANDARD_DEADLINE, |(_, _, deadline)| *deadline)
}
pub(crate) async fn enforce(req: Request, next: Next) -> Response {
let method = req.method().clone();
let template = req.extensions().get::<MatchedPath>().cloned();
let unmatched = template.is_none().then(|| req.uri().path().to_owned());
let deadline = deadline_for(method.as_str(), template.as_ref().map(MatchedPath::as_str));
match tokio::time::timeout(deadline, next.run(req)).await {
Ok(response) => response,
Err(_elapsed) => {
let route = template.as_ref().map_or(
unmatched.as_deref().unwrap_or_default(),
MatchedPath::as_str,
);
deadline_exceeded(method.as_str(), route, deadline)
}
}
}
fn deadline_exceeded(method: &str, route: &str, deadline: Duration) -> Response {
let secs = deadline.as_secs();
tracing::warn!(
method,
route,
deadline_secs = secs,
"request exceeded its server-side deadline (#8476)"
);
let body = serde_json::json!({
"error": format!(
"{method} {route} exceeded the daemon's {secs} s server-side deadline; \
the request was abandoned (#8476)"
),
"route": route,
"deadline_secs": secs,
});
(StatusCode::GATEWAY_TIMEOUT, Json(body)).into_response()
}
#[cfg(test)]
#[path = "request_deadline_tests.rs"]
mod tests;