From cf30bcf3f9876d573ed011c250aa6c4e33547b19 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Tue, 31 Mar 2026 11:14:20 +0000 Subject: [PATCH] 5x request size limit for raw app bundle uploads (#8640) * feat: 5x request size limit for raw app bundle uploads Raw app bundle endpoints (create_raw, update_raw) now get 5x the configured request size limit. Also improves error messages when multipart uploads exceed the limit to include the actual limit and mention it's adjustable in instance settings. Co-Authored-By: Claude Opus 4.6 (1M context) * fix: mention size limit as possible cause, not definitive Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- backend/windmill-api/src/apps.rs | 18 +++++++++++++----- backend/windmill-api/src/lib.rs | 4 +++- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/backend/windmill-api/src/apps.rs b/backend/windmill-api/src/apps.rs index 33a68409a0..e9f262881a 100644 --- a/backend/windmill-api/src/apps.rs +++ b/backend/windmill-api/src/apps.rs @@ -79,7 +79,7 @@ use windmill_common::{jwt, oauth2::HmacSha256, variables::get_workspace_key}; #[cfg(feature = "parquet")] use windmill_types::s3::{S3Object, S3Permission}; -pub fn workspaced_service() -> Router { +pub fn workspaced_service(raw_app_body_limit: usize) -> Router { Router::new() .route("/list", get(list_apps)) .route("/list_search", get(list_search_apps)) @@ -95,10 +95,16 @@ pub fn workspaced_service() -> Router { .route("/get_data/v/{*id}", get(get_raw_app_data)) .route("/exists/{*path}", get(exists_app)) .route("/update/{*path}", post(update_app)) - .route("/update_raw/{*path}", post(update_app_raw)) + .route( + "/update_raw/{*path}", + post(update_app_raw).layer(axum::extract::DefaultBodyLimit::max(raw_app_body_limit)), + ) .route("/delete/{*path}", delete(delete_app)) .route("/create", post(create_app)) - .route("/create_raw", post(create_app_raw)) + .route( + "/create_raw", + post(create_app_raw).layer(axum::extract::DefaultBodyLimit::max(raw_app_body_limit)), + ) .route("/history/p/{*path}", get(get_app_history)) .route("/get_latest_version/{*path}", get(get_latest_version)) .route( @@ -1010,18 +1016,20 @@ macro_rules! process_app_multipart { let mut saved_app = None; let mut uploaded_js = false; + let request_size_limit_mb = *crate::REQUEST_SIZE_LIMIT.read().await / (1024 * 1024); + let raw_app_limit_mb = request_size_limit_mb * 5; let mut multipart = $multipart; while let Some(field) = multipart .next_field() .await - .map_err(|e| Error::BadRequest(format!("failed to read multipart field: {e}")))? + .map_err(|e| Error::BadRequest(format!("failed to read multipart field: {e}. Could be due to the request size limit for raw app bundles which is {raw_app_limit_mb}MB (adjustable in instance settings)")))? { let name = field .name() .ok_or_else(|| Error::BadRequest("multipart field missing name".to_string()))? .to_string(); let data = field.bytes().await.map_err(|e| { - Error::BadRequest(format!("failed to read multipart stream: {e}")) + Error::BadRequest(format!("failed to read multipart stream: {e}. Could be due to the request size limit for raw app bundles which is {raw_app_limit_mb}MB (adjustable in instance settings)")) })?; if name == "app" { let app = serde_json::from_slice(&data).map_err(to_anyhow)?; diff --git a/backend/windmill-api/src/lib.rs b/backend/windmill-api/src/lib.rs index cfe9080e1a..fd356de67f 100644 --- a/backend/windmill-api/src/lib.rs +++ b/backend/windmill-api/src/lib.rs @@ -379,6 +379,8 @@ pub async fn run_server( REQUEST_SIZE_LIMIT.read().await.clone(), )); + let request_size_limit = REQUEST_SIZE_LIMIT.read().await.clone(); + let cors = CorsLayer::new() .allow_methods([http::Method::GET, http::Method::POST, http::Method::DELETE]) .allow_headers([http::header::CONTENT_TYPE, http::header::AUTHORIZATION]) @@ -533,7 +535,7 @@ pub async fn run_server( Router::new() // Reordered alphabetically .nest("/acls", granular_acls::workspaced_service()) - .nest("/apps", apps::workspaced_service()) + .nest("/apps", apps::workspaced_service(request_size_limit * 5)) .nest("/assets", windmill_api_assets::workspaced_service()) .nest("/audit", audit::workspaced_service()) .nest("/capture", capture::workspaced_service())