From 5dc43c400a8d8131eb77a4e91267c1cf37594c27 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Wed, 26 Aug 2026 00:31:21 +0200 Subject: [PATCH] report the EE gate instead of a 500 on restart flow at step (#10846) Claude-Session: https://claude.ai/code/session_01CbayDTXcGCTYuE9m56BRag Co-authored-by: Claude Opus 5 (1M context) --- .../tests/jobs_authed.rs | 19 +++++++++++++++---- backend/windmill-api/src/jobs.rs | 15 ++++++++------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/backend/windmill-api-integration-tests/tests/jobs_authed.rs b/backend/windmill-api-integration-tests/tests/jobs_authed.rs index cd60275afc..c3da2f3fb9 100644 --- a/backend/windmill-api-integration-tests/tests/jobs_authed.rs +++ b/backend/windmill-api-integration-tests/tests/jobs_authed.rs @@ -286,13 +286,24 @@ async fn test_jobs_authed_reachability(db: Pool) -> anyhow::Result<()> "GET /jobs/result_by_id", ); + // Sent the way the generated client sends it. A handler whose `Path` tuple has drifted from + // the route is rejected by axum before it runs, which surfaces as a routing error rather + // than the handler's own answer, so reaching the handler is what this pins. let resp = authed(client().post(format!("{base}/restart/f/{fake}"))) + .json(&json!({ "step_id": "a" })) .send() .await?; - assert_route_reachable( - resp.status().as_u16(), - &resp.text().await?, - "POST /jobs/restart/f", + let status = resp.status().as_u16(); + let body = resp.text().await?; + assert_route_reachable(status, &body, "POST /jobs/restart/f"); + assert!( + !body.contains("path arguments"), + "POST /jobs/restart/f never reached its handler: {status} {body}", + ); + #[cfg(not(feature = "enterprise"))] + assert!( + body.contains("only available in enterprise version"), + "POST /jobs/restart/f must report the enterprise gate outside EE: {status} {body}", ); let resp = authed(client().post(format!("{base}/run/workflow_as_code/{fake}/main"))) diff --git a/backend/windmill-api/src/jobs.rs b/backend/windmill-api/src/jobs.rs index 513341b6c1..a30cffbfbf 100644 --- a/backend/windmill-api/src/jobs.rs +++ b/backend/windmill-api/src/jobs.rs @@ -6559,17 +6559,18 @@ pub async fn run_flow_by_version_inner( Ok((uuid, early_return, has_failure_module)) } +/// Path parameters of `POST /w/{workspace}/jobs/restart/f/{job_id}`, shared by the CE and EE +/// handlers. Axum only checks the tuple against the route at request time and rejects a +/// mismatch with an opaque 500 before the handler runs, so both must be declared from here: +/// an arity that drifts from the route hides the handler behind what reads as a broken route. +type RestartFlowPath = Path<(String, Uuid)>; + #[cfg(not(feature = "enterprise"))] pub async fn restart_flow( _authed: ApiAuthed, Extension(_db): Extension, Extension(_user_db): Extension, - Path((_w_id, _job_id, _step_id, _branch_or_iteration_n)): Path<( - String, - Uuid, - String, - Option, - )>, + Path((_w_id, _job_id)): RestartFlowPath, Query(_run_query): Query, ) -> error::Result<(StatusCode, String)> { return Err(Error::BadRequest( @@ -6798,7 +6799,7 @@ pub async fn restart_flow( authed: ApiAuthed, Extension(db): Extension, Extension(user_db): Extension, - Path((w_id, job_id)): Path<(String, Uuid)>, + Path((w_id, job_id)): RestartFlowPath, Query(run_query): Query, Json(RestartFlowRequestBody { step_id,