hive-c0re: wire routes individually — routes! macro panics on multi-path batches

This commit is contained in:
damocles 2026-07-31 23:28:34 +02:00 committed by mara
commit 2c51fb70d0

View file

@ -141,69 +141,84 @@ pub async fn serve(
// No static fallback — the gateway owns the dist; unmatched paths 404.
;
// `routes!()` folds every handler it's given into ONE shared
// `MethodRouter` for the whole macro invocation — it does NOT group
// by path internally, so passing it handlers for more than one
// distinct path panics at runtime ("Overlapping method route")
// the moment two of them share an HTTP method, which most
// GET-vs-GET or POST-vs-POST pairs across different paths do. Each
// call below is therefore scoped to exactly one path — two handlers
// in the same call only when they genuinely share a path with
// different methods (`schedules::api_schedules`/`post_schedule_new`
// on `/api/schedules`, `matrix_accounts::get_github_account`/
// `post_github_account` on `/api/github-account`) — chained via
// repeated `.routes(...)` calls instead of one giant `routes!(...)`
// with everything in it.
let (router, api) = OpenApiRouter::<AppState>::with_openapi(ApiDoc::openapi())
.routes(routes!(health::get_health_live))
.routes(routes!(health::get_health_ready))
.routes(routes!(journal::get_journal))
.routes(routes!(journal::get_journal_host))
.routes(routes!(state_snapshot::api_state))
.routes(routes!(state_files::get_state_file))
.routes(routes!(matrix_accounts::get_matrix_accounts))
.routes(routes!(matrix_accounts::post_matrix_account_login))
.routes(routes!(
health::get_health_live,
health::get_health_ready,
journal::get_journal,
journal::get_journal_host,
state_snapshot::api_state,
state_files::get_state_file,
matrix_accounts::get_matrix_accounts,
matrix_accounts::post_matrix_account_login,
matrix_accounts::post_github_account,
matrix_accounts::get_github_account,
extra_forges::get_extra_forges,
extra_forges::post_extra_forge_account,
misc_api::api_operator_inbox,
misc_api::api_stats_hive,
misc_api::api_container_resources,
misc_api::api_audit_log,
misc_api::post_mark_all_read,
misc_api::post_request_spawn,
misc_api::post_op_send,
build_logs::get_build_logs_all,
build_logs::get_build_log_for_node,
build_logs::get_build_log_raw_for_node,
build_logs::get_build_logs_agent,
build_logs::get_build_log_full,
build_logs::get_build_log_raw,
topology::post_set_parent,
topology::post_set_parent_bulk,
permissions::get_tool_groups,
permissions::post_tool_groups,
permissions::get_capabilities,
permissions::post_capabilities,
permissions::post_permissions,
permissions::get_stale_permissions,
permissions::delete_agent_permissions,
schedules::api_schedules,
schedules::post_schedule_new,
schedules::patch_schedule,
schedules::post_schedule_cancel,
schedules::post_schedule_pause,
schedules::post_schedule_resume,
schedules::post_schedule_fire_now,
schedules::post_rebuild_queue_cancel,
webhook::post_webhook_knowledge,
webhook::post_webhook_config_pr,
approvals::post_approve,
approvals::post_deny,
lifecycle_ops::post_destroy,
lifecycle_ops::post_kill,
lifecycle_ops::post_restart,
lifecycle_ops::post_start,
lifecycle_ops::post_rebuild,
lifecycle_ops::post_pause,
lifecycle_ops::post_resume,
lifecycle_ops::post_resource_limits,
lifecycle_ops::post_update_all,
infra_containers::post_infra_container,
questions::post_answer_question,
questions::post_cancel_question,
tombstones::post_purge_tombstone,
meta_inputs::post_meta_update,
matrix_accounts::get_github_account
))
.routes(routes!(extra_forges::get_extra_forges))
.routes(routes!(extra_forges::post_extra_forge_account))
.routes(routes!(misc_api::api_operator_inbox))
.routes(routes!(misc_api::api_stats_hive))
.routes(routes!(misc_api::api_container_resources))
.routes(routes!(misc_api::api_audit_log))
.routes(routes!(misc_api::post_mark_all_read))
.routes(routes!(misc_api::post_request_spawn))
.routes(routes!(misc_api::post_op_send))
.routes(routes!(build_logs::get_build_logs_all))
.routes(routes!(build_logs::get_build_log_for_node))
.routes(routes!(build_logs::get_build_log_raw_for_node))
.routes(routes!(build_logs::get_build_logs_agent))
.routes(routes!(build_logs::get_build_log_full))
.routes(routes!(build_logs::get_build_log_raw))
.routes(routes!(topology::post_set_parent))
.routes(routes!(topology::post_set_parent_bulk))
.routes(routes!(permissions::get_tool_groups))
.routes(routes!(permissions::post_tool_groups))
.routes(routes!(permissions::get_capabilities))
.routes(routes!(permissions::post_capabilities))
.routes(routes!(permissions::post_permissions))
.routes(routes!(permissions::get_stale_permissions))
.routes(routes!(permissions::delete_agent_permissions))
.routes(routes!(
schedules::api_schedules,
schedules::post_schedule_new
))
.routes(routes!(schedules::patch_schedule))
.routes(routes!(schedules::post_schedule_cancel))
.routes(routes!(schedules::post_schedule_pause))
.routes(routes!(schedules::post_schedule_resume))
.routes(routes!(schedules::post_schedule_fire_now))
.routes(routes!(schedules::post_rebuild_queue_cancel))
.routes(routes!(webhook::post_webhook_knowledge))
.routes(routes!(webhook::post_webhook_config_pr))
.routes(routes!(approvals::post_approve))
.routes(routes!(approvals::post_deny))
.routes(routes!(lifecycle_ops::post_destroy))
.routes(routes!(lifecycle_ops::post_kill))
.routes(routes!(lifecycle_ops::post_restart))
.routes(routes!(lifecycle_ops::post_start))
.routes(routes!(lifecycle_ops::post_rebuild))
.routes(routes!(lifecycle_ops::post_pause))
.routes(routes!(lifecycle_ops::post_resume))
.routes(routes!(lifecycle_ops::post_resource_limits))
.routes(routes!(lifecycle_ops::post_update_all))
.routes(routes!(infra_containers::post_infra_container))
.routes(routes!(questions::post_answer_question))
.routes(routes!(questions::post_cancel_question))
.routes(routes!(tombstones::post_purge_tombstone))
.routes(routes!(meta_inputs::post_meta_update))
.merge(app.into())
.split_for_parts();
let app = router
@ -360,13 +375,84 @@ mod tests {
mod router_build_probe {
use super::*;
/// Regression test for the exact panic this file's `serve()` used to
/// hit at startup: `routes!()` folds every handler passed to ONE
/// macro invocation into a single shared `MethodRouter`, so two
/// handlers on different paths but the same HTTP method panic with
/// "Overlapping method route" the moment they're in the same
/// `routes!(...)` call — see `serve()`'s comment above its own
/// `.routes(...)` chain. This mirrors that exact chain (one
/// `.routes()` call per distinct path, multi-method pairs grouped
/// only where they share a path) so a future regression that
/// accidentally merges two single-path calls back into one
/// multi-path `routes!(...)` call fails here instead of at daemon
/// startup.
#[test]
fn probe_multi_path_routes_macro_does_not_panic() {
let _ = OpenApiRouter::<AppState>::with_openapi(ApiDoc::openapi()).routes(routes!(
health::get_health_live,
health::get_health_ready,
journal::get_journal,
journal::get_journal_host,
));
let _ = OpenApiRouter::<AppState>::with_openapi(ApiDoc::openapi())
.routes(routes!(health::get_health_live))
.routes(routes!(health::get_health_ready))
.routes(routes!(journal::get_journal))
.routes(routes!(journal::get_journal_host))
.routes(routes!(state_snapshot::api_state))
.routes(routes!(state_files::get_state_file))
.routes(routes!(matrix_accounts::get_matrix_accounts))
.routes(routes!(matrix_accounts::post_matrix_account_login))
.routes(routes!(
matrix_accounts::post_github_account,
matrix_accounts::get_github_account
))
.routes(routes!(extra_forges::get_extra_forges))
.routes(routes!(extra_forges::post_extra_forge_account))
.routes(routes!(misc_api::api_operator_inbox))
.routes(routes!(misc_api::api_stats_hive))
.routes(routes!(misc_api::api_container_resources))
.routes(routes!(misc_api::api_audit_log))
.routes(routes!(misc_api::post_mark_all_read))
.routes(routes!(misc_api::post_request_spawn))
.routes(routes!(misc_api::post_op_send))
.routes(routes!(build_logs::get_build_logs_all))
.routes(routes!(build_logs::get_build_log_for_node))
.routes(routes!(build_logs::get_build_log_raw_for_node))
.routes(routes!(build_logs::get_build_logs_agent))
.routes(routes!(build_logs::get_build_log_full))
.routes(routes!(build_logs::get_build_log_raw))
.routes(routes!(topology::post_set_parent))
.routes(routes!(topology::post_set_parent_bulk))
.routes(routes!(permissions::get_tool_groups))
.routes(routes!(permissions::post_tool_groups))
.routes(routes!(permissions::get_capabilities))
.routes(routes!(permissions::post_capabilities))
.routes(routes!(permissions::post_permissions))
.routes(routes!(permissions::get_stale_permissions))
.routes(routes!(permissions::delete_agent_permissions))
.routes(routes!(
schedules::api_schedules,
schedules::post_schedule_new
))
.routes(routes!(schedules::patch_schedule))
.routes(routes!(schedules::post_schedule_cancel))
.routes(routes!(schedules::post_schedule_pause))
.routes(routes!(schedules::post_schedule_resume))
.routes(routes!(schedules::post_schedule_fire_now))
.routes(routes!(schedules::post_rebuild_queue_cancel))
.routes(routes!(webhook::post_webhook_knowledge))
.routes(routes!(webhook::post_webhook_config_pr))
.routes(routes!(approvals::post_approve))
.routes(routes!(approvals::post_deny))
.routes(routes!(lifecycle_ops::post_destroy))
.routes(routes!(lifecycle_ops::post_kill))
.routes(routes!(lifecycle_ops::post_restart))
.routes(routes!(lifecycle_ops::post_start))
.routes(routes!(lifecycle_ops::post_rebuild))
.routes(routes!(lifecycle_ops::post_pause))
.routes(routes!(lifecycle_ops::post_resume))
.routes(routes!(lifecycle_ops::post_resource_limits))
.routes(routes!(lifecycle_ops::post_update_all))
.routes(routes!(infra_containers::post_infra_container))
.routes(routes!(questions::post_answer_question))
.routes(routes!(questions::post_cancel_question))
.routes(routes!(tombstones::post_purge_tombstone))
.routes(routes!(meta_inputs::post_meta_update));
}
}