From c23f9fc62d56712be38546a57e6f7dc8a5146921 Mon Sep 17 00:00:00 2001 From: ybai08 <271969568+ybai08@users.noreply.github.com> Date: Sun, 18 Oct 2026 14:45:28 -0500 Subject: [PATCH] Fixed requests staying pending after a failing post-request script https://github.com/Julien-cpsn/ATAC/issues/209 When the post-request script failed, the HTTP, WebSocket and MQTT senders returned the error before marking the request as finished, so the TUI showed it as pending until ATAC was restarted. The script's console output was also thrown away, so the "(CONSOLE)" error pointed at an empty or stale console. The request is now marked as finished before the script error is returned, the script error is kept as the post-request console output, and the CLI prints it with --console. --- src/app/business_logic/request/http/send.rs | 67 ++++++++++++++++++++- src/app/business_logic/request/mqtt/send.rs | 32 ++++++++-- src/app/business_logic/request/send.rs | 9 +-- src/app/business_logic/request/ws/send.rs | 51 ++++++++++++++-- src/cli/cli_logic/request/send.rs | 17 +++++- 5 files changed, 158 insertions(+), 18 deletions(-) diff --git a/src/app/business_logic/request/http/send.rs b/src/app/business_logic/request/http/send.rs index 5e5f092..a11239e 100644 --- a/src/app/business_logic/request/http/send.rs +++ b/src/app/business_logic/request/http/send.rs @@ -157,7 +157,7 @@ pub async fn send_http_request(prepared_request: reqwest_middleware::RequestBuil /* POST-REQUEST SCRIPT */ - let (modified_response, post_request_output) = App::handle_post_request_script(&request, response, env)?; + let (modified_response, post_request_output) = App::handle_post_request_script(&request, response, env); drop(request); @@ -169,5 +169,66 @@ pub async fn send_http_request(prepared_request: reqwest_middleware::RequestBuil request.cancellation_token = CancellationToken::new(); } - return Ok(modified_response); -} \ No newline at end of file + return modified_response; +} + +#[cfg(test)] +mod tests { + use std::io::{Read, Write}; + use std::net::TcpListener; + use std::sync::Arc; + use parking_lot::RwLock; + use crate::app::business_logic::request::http::send::send_http_request; + use crate::app::business_logic::request::send::RequestResponseError; + use crate::models::request::Request; + + /// Answers a single request with a small 200 response, then closes the connection + fn one_response_server() -> String { + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let address = listener.local_addr().unwrap(); + + std::thread::spawn(move || { + let (mut stream, _) = listener.accept().unwrap(); + let mut buffer = [0; 4096]; + let mut received = Vec::new(); + + while !received.windows(4).any(|window| window == b"\r\n\r\n") { + let read = stream.read(&mut buffer).unwrap(); + + if read == 0 { + break; + } + + received.extend_from_slice(&buffer[..read]); + } + + stream.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok").unwrap(); + }); + + format!("http://{address}/") + } + + #[tokio::test] + async fn failing_post_request_script_does_not_leave_the_request_pending() { + let url = one_response_server(); + + let mut request = Request { + url: url.clone(), + ..Default::default() + }; + request.scripts.post_request_script = Some(String::from(r#"throw new Error("post-request script failure");"#)); + + let local_request = Arc::new(RwLock::new(request)); + let prepared_request = reqwest_middleware::ClientBuilder::new(reqwest::Client::new()).build().get(&url); + + let response = send_http_request(prepared_request, local_request.clone(), &None).await; + + assert!(matches!(response, Err(RequestResponseError::PostRequestScript))); + + let request = local_request.read(); + assert!(!request.is_pending); + + let console_output = request.console_output.post_request_output.as_deref().unwrap_or_default(); + assert!(console_output.contains("post-request script failure"), "{console_output:?}"); + } +} diff --git a/src/app/business_logic/request/mqtt/send.rs b/src/app/business_logic/request/mqtt/send.rs index 08fe8f4..229ae86 100644 --- a/src/app/business_logic/request/mqtt/send.rs +++ b/src/app/business_logic/request/mqtt/send.rs @@ -180,7 +180,7 @@ pub async fn send_mqtt_request(prepared_request: PreparedMqttRequest, local_requ /* POST-REQUEST SCRIPT */ let request = local_request.read(); - let (modified_response, post_request_output) = App::handle_post_request_script(&request, response, env)?; + let (modified_response, post_request_output) = App::handle_post_request_script(&request, response, env); drop(request); let mut request = local_request.write(); @@ -189,6 +189,8 @@ pub async fn send_mqtt_request(prepared_request: PreparedMqttRequest, local_requ request.is_pending = false; request.cancellation_token = CancellationToken::new(); + let modified_response = modified_response?; + let mqtt_request = request.get_mqtt_request_mut().unwrap(); mqtt_request.messages = vec![]; @@ -385,11 +387,11 @@ fn error_response(error: String) -> RequestResponse { mod tests { use std::sync::Arc; use indexmap::IndexMap; - use parking_lot::RwLock; + use parking_lot::{Mutex, RwLock}; use crate::app::app::App; use crate::app::business_logic::request::mqtt::client::PreparedMqttRequest; - use crate::app::business_logic::request::mqtt::send::mqtt_publish; - use crate::app::business_logic::request::send::PrepareRequestError; + use crate::app::business_logic::request::mqtt::send::{mqtt_publish, send_mqtt_request}; + use crate::app::business_logic::request::send::{PrepareRequestError, RequestResponseError}; use crate::models::auth::auth::Auth; use crate::models::auth::basic::BasicAuth; use crate::models::auth::bearer_token::BearerToken; @@ -613,6 +615,28 @@ mod tests { assert!(mqtt_request.messages.is_empty()); } + #[tokio::test] + async fn failing_post_request_script_does_not_leave_the_request_pending() { + // Nothing listens on this port, so the connection is refused + let port = std::net::TcpListener::bind("127.0.0.1:0").unwrap().local_addr().unwrap().port(); + + let mut failing = request(&format!("mqtt://127.0.0.1:{port}"), |_| {}); + failing.scripts.post_request_script = Some(String::from(r#"throw new Error("post-request script failure");"#)); + + let prepared = prepare(&mut failing).unwrap(); + let local_request = Arc::new(RwLock::new(failing)); + + let response = send_mqtt_request(prepared, local_request.clone(), &None, Arc::new(Mutex::new(false))).await; + + assert!(matches!(response, Err(RequestResponseError::PostRequestScript))); + + let request = local_request.read(); + assert!(!request.is_pending); + + let console_output = request.console_output.post_request_output.as_deref().unwrap_or_default(); + assert!(console_output.contains("post-request script failure"), "{console_output:?}"); + } + #[test] fn publish_after_the_connection_ended_is_not_logged() { let (mut mqtt_request, receiver) = connected(MqttVersion::V3_1_1, 100); diff --git a/src/app/business_logic/request/send.rs b/src/app/business_logic/request/send.rs index 597397b..3623346 100644 --- a/src/app/business_logic/request/send.rs +++ b/src/app/business_logic/request/send.rs @@ -357,10 +357,11 @@ impl App<'_> { } } - pub fn handle_post_request_script(request: &Request, response: RequestResponse, env: &Option>>) -> anyhow::Result<(RequestResponse, Option), RequestResponseError> { + /// The console output is returned even when the script fails, so the error can be read in the console + pub fn handle_post_request_script(request: &Request, response: RequestResponse, env: &Option>>) -> (Result, Option) { match &request.scripts.post_request_script { None => { - Ok((response, None)) + (Ok(response), None) }, Some(post_request_script) => { let env_values = match &env { @@ -386,8 +387,8 @@ impl App<'_> { } match result_response { - None => Err(PostRequestScript), - Some(result_response) => Ok((result_response, Some(result_console_output))) + None => (Err(PostRequestScript), Some(result_console_output)), + Some(result_response) => (Ok(result_response), Some(result_console_output)) } } } diff --git a/src/app/business_logic/request/ws/send.rs b/src/app/business_logic/request/ws/send.rs index bd50758..ca57420 100644 --- a/src/app/business_logic/request/ws/send.rs +++ b/src/app/business_logic/request/ws/send.rs @@ -132,17 +132,19 @@ pub async fn send_ws_request(prepared_request: reqwest_middleware::RequestBuilde /* POST-REQUEST SCRIPT */ - let (modified_response, post_request_output) = App::handle_post_request_script(&request, response, env)?; + let (modified_response, post_request_output) = App::handle_post_request_script(&request, response, env); drop(request); - { + let modified_response = { let mut request = local_request.write(); request.console_output.post_request_output = post_request_output; request.is_pending = false; request.cancellation_token = CancellationToken::new(); + let modified_response = modified_response?; + let ws_request = request.get_ws_request_mut().unwrap(); ws_request.messages = vec![]; @@ -152,7 +154,9 @@ pub async fn send_ws_request(prepared_request: reqwest_middleware::RequestBuilde else { ws_request.is_connected = true; } - } + + modified_response + }; let local_request = local_request.clone(); let mut request = local_request.write(); @@ -217,4 +221,43 @@ pub async fn send_ws_request(prepared_request: reqwest_middleware::RequestBuilde }); return Ok(modified_response); -} \ No newline at end of file +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + use parking_lot::{Mutex, RwLock}; + use crate::app::business_logic::request::send::RequestResponseError; + use crate::app::business_logic::request::ws::send::send_ws_request; + use crate::models::protocol::protocol::Protocol; + use crate::models::protocol::ws::ws::WsRequest; + use crate::models::request::Request; + + #[tokio::test] + async fn failing_post_request_script_does_not_leave_the_request_pending() { + // Nothing listens on this port, so the connection is refused + let port = std::net::TcpListener::bind("127.0.0.1:0").unwrap().local_addr().unwrap().port(); + let url = format!("http://127.0.0.1:{port}/"); + + let mut request = Request { + url: url.clone(), + protocol: Protocol::WsRequest(WsRequest::default()), + ..Default::default() + }; + request.scripts.post_request_script = Some(String::from(r#"throw new Error("post-request script failure");"#)); + + let local_request = Arc::new(RwLock::new(request)); + let prepared_request = reqwest_middleware::ClientBuilder::new(reqwest::Client::new()).build().get(&url); + + let response = send_ws_request(prepared_request, local_request.clone(), &None, Arc::new(Mutex::new(false))).await; + + assert!(matches!(response, Err(RequestResponseError::PostRequestScript))); + + let request = local_request.read(); + assert!(!request.is_pending); + assert!(!request.get_ws_request().unwrap().is_connected); + + let console_output = request.console_output.post_request_output.as_deref().unwrap_or_default(); + assert!(console_output.contains("post-request script failure"), "{console_output:?}"); + } +} diff --git a/src/cli/cli_logic/request/send.rs b/src/cli/cli_logic/request/send.rs index b0e8f4f..799be51 100644 --- a/src/cli/cli_logic/request/send.rs +++ b/src/cli/cli_logic/request/send.rs @@ -93,12 +93,23 @@ impl App<'_> { let local_env = self.get_selected_env_as_local(); let response = match (&protocol, prepared_request) { - (Protocol::HttpRequest(_), PreparedRequest::Reqwest(prepared_request)) => send_http_request(prepared_request, local_request.clone(), &local_env).await?, - (Protocol::WsRequest(_), PreparedRequest::Reqwest(prepared_request)) => send_ws_request(prepared_request, local_request.clone(), &local_env, self.received_response.clone()).await?, - (Protocol::MqttRequest(_), PreparedRequest::Mqtt(prepared_request)) => send_mqtt_request(prepared_request, local_request.clone(), &local_env, self.received_response.clone()).await?, + (Protocol::HttpRequest(_), PreparedRequest::Reqwest(prepared_request)) => send_http_request(prepared_request, local_request.clone(), &local_env).await, + (Protocol::WsRequest(_), PreparedRequest::Reqwest(prepared_request)) => send_ws_request(prepared_request, local_request.clone(), &local_env, self.received_response.clone()).await, + (Protocol::MqttRequest(_), PreparedRequest::Mqtt(prepared_request)) => send_mqtt_request(prepared_request, local_request.clone(), &local_env, self.received_response.clone()).await, _ => unreachable!() }; + let response = match response { + Ok(response) => response, + Err(error) => { + if send_command.console && let Some(post_request_output) = &local_request.read().console_output.post_request_output { + println!("{}", post_request_output); + } + + return Err(anyhow!(error)); + } + }; + let request = local_request.read(); if send_command.status_code {