From 8da9ca63a31c59249ae2d0b07e7b5962207fada1 Mon Sep 17 00:00:00 2001 From: subrata71 Date: Fri, 1 May 2026 02:20:03 +0600 Subject: [PATCH 1/3] fix(actions): replace generic "Response not valid" with actionable error messages The catch-all in executePluginActionSaga discarded the original error (Axios timeout, network failure, server validation rejection) and replaced it with the opaque "Response not valid" message, making production debugging impossible for customers in view mode. Client: - Add extractExecutionErrorMessage() to categorise Axios transport errors vs server envelope errors vs unknown failures - Surface the specific message through the runActionSaga error cascade instead of falling through to "An unexpected error occurred" - Trigger execution path inherits the fix automatically Server: - Replace result.setRequest(null) in view mode with sanitizeRequestForViewMode() that retains actionId, requestedAt, and httpMethod while stripping sensitive fields (body, headers, URL, params) - Upgrade execution error logging from debug/println/printStackTrace to structured SLF4J warn/error with action identity --- .../sagas/ActionExecution/PluginActionSaga.ts | 24 +++++- .../sagas/ActionExecution/errorUtils.test.ts | 74 +++++++++++++++++++ .../src/sagas/ActionExecution/errorUtils.ts | 40 ++++++++++ .../helpers/RestAPIActivateUtils.java | 5 +- .../com/external/plugins/RestApiPlugin.java | 9 ++- .../ce/ActionExecutionSolutionCEImpl.java | 32 +++++++- .../ce/ActionExecutionSolutionCEImplTest.java | 40 ++++++++++ 7 files changed, 213 insertions(+), 11 deletions(-) create mode 100644 app/client/src/sagas/ActionExecution/errorUtils.test.ts diff --git a/app/client/src/sagas/ActionExecution/PluginActionSaga.ts b/app/client/src/sagas/ActionExecution/PluginActionSaga.ts index 00fd7bffb4d9..e1db4449d5b8 100644 --- a/app/client/src/sagas/ActionExecution/PluginActionSaga.ts +++ b/app/client/src/sagas/ActionExecution/PluginActionSaga.ts @@ -113,6 +113,7 @@ import { FileDataTypes } from "WidgetProvider/types"; import { hideDebuggerErrors } from "actions/debuggerActions"; import { ActionValidationError, + extractExecutionErrorMessage, getErrorAsString, PluginActionExecutionError, PluginTriggerFailureError, @@ -873,6 +874,18 @@ export function* runActionSaga( } : undefined; + // When the server response was lost (network/timeout/parse failure), the + // caught PluginActionExecutionError carries the specific transport message + // from extractExecutionErrorMessage. Surface it instead of the generic + // fallback so users and the debugger console see what actually went wrong. + const transportError = + error.message && error.message !== "An unexpected error occurred" + ? { + name: "PluginExecutionError", + message: error.message, + } + : undefined; + const defaultError = { name: "PluginExecutionError", message: "An unexpected error occurred", @@ -888,7 +901,11 @@ export function* runActionSaga( if (isError) { error = - readableError || payloadBodyError || clientDefinedError || defaultError; + readableError || + payloadBodyError || + clientDefinedError || + transportError || + defaultError; // In case of debugger, both the current error message // and the readableError needs to be present, @@ -1551,7 +1568,10 @@ function* executePluginActionSaga( ); } - throw new PluginActionExecutionError("Response not valid", false); + throw new PluginActionExecutionError( + extractExecutionErrorMessage(e), + false, + ); } } diff --git a/app/client/src/sagas/ActionExecution/errorUtils.test.ts b/app/client/src/sagas/ActionExecution/errorUtils.test.ts new file mode 100644 index 000000000000..c35fd94dfb0e --- /dev/null +++ b/app/client/src/sagas/ActionExecution/errorUtils.test.ts @@ -0,0 +1,74 @@ +import { extractExecutionErrorMessage } from "./errorUtils"; + +describe("extractExecutionErrorMessage", () => { + it("returns timeout message for Axios ECONNABORTED with timeout pattern", () => { + const axiosError = Object.assign(new Error("timeout of 10000ms exceeded"), { + code: "ECONNABORTED", + isAxiosError: true, + }); + + expect(extractExecutionErrorMessage(axiosError)).toBe( + "Action execution timed out. Try increasing the timeout in the action settings.", + ); + }); + + it("returns network error message for Axios Network Error", () => { + const axiosError = Object.assign(new Error("Network Error"), { + isAxiosError: true, + }); + + expect(extractExecutionErrorMessage(axiosError)).toBe( + "Network error: could not reach the Appsmith server. Check your connection.", + ); + }); + + it("returns Axios message for other Axios errors", () => { + const axiosError = Object.assign( + new Error("Request failed with status code 502"), + { isAxiosError: true }, + ); + + expect(extractExecutionErrorMessage(axiosError)).toBe( + "Request failed: Request failed with status code 502", + ); + }); + + it("returns Axios message for ECONNABORTED without timeout pattern", () => { + const axiosError = Object.assign(new Error("connection aborted"), { + code: "ECONNABORTED", + }); + + expect(extractExecutionErrorMessage(axiosError)).toBe( + "Request failed: connection aborted", + ); + }); + + it("returns server error message from validateResponse errors", () => { + const serverError = new Error("Organization not found"); + + expect(extractExecutionErrorMessage(serverError)).toBe( + "Organization not found", + ); + }); + + it("returns 'Response not valid' for non-Error values", () => { + expect(extractExecutionErrorMessage("string error")).toBe( + "Response not valid", + ); + expect(extractExecutionErrorMessage(null)).toBe("Response not valid"); + expect(extractExecutionErrorMessage(undefined)).toBe("Response not valid"); + expect(extractExecutionErrorMessage(42)).toBe("Response not valid"); + }); + + it("returns 'Response not valid' for Error with empty message", () => { + expect(extractExecutionErrorMessage(new Error(""))).toBe( + "Response not valid", + ); + }); + + it("returns the error message for a plain Error", () => { + const error = new Error("Something went wrong"); + + expect(extractExecutionErrorMessage(error)).toBe("Something went wrong"); + }); +}); diff --git a/app/client/src/sagas/ActionExecution/errorUtils.ts b/app/client/src/sagas/ActionExecution/errorUtils.ts index 2f2637f937b9..96457215e3cb 100644 --- a/app/client/src/sagas/ActionExecution/errorUtils.ts +++ b/app/client/src/sagas/ActionExecution/errorUtils.ts @@ -120,3 +120,43 @@ export class UserCancelledActionExecutionError extends PluginActionExecutionErro export const getErrorAsString = (error: unknown): string => { return isString(error) ? error : JSON.stringify(error); }; + +const AXIOS_TIMEOUT_REGEX = /timeout of \d+ms exceeded/; + +/** + * Extracts a meaningful, user-facing error message from the caught exception + * in the action execution path. Categorises Axios transport errors, server + * envelope errors (from validateResponse), and falls back gracefully. + * + * Security: only surfaces our own server messages or Axios transport strings — + * never raw upstream API bodies or credentials. + */ +export function extractExecutionErrorMessage(e: unknown): string { + if (!(e instanceof Error)) { + return "Response not valid"; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const axiosLike = e as any; + + if (axiosLike.isAxiosError || axiosLike.code === "ECONNABORTED") { + if ( + axiosLike.code === "ECONNABORTED" && + AXIOS_TIMEOUT_REGEX.test(axiosLike.message) + ) { + return "Action execution timed out. Try increasing the timeout in the action settings."; + } + + if (axiosLike.message === "Network Error") { + return "Network error: could not reach the Appsmith server. Check your connection."; + } + + return `Request failed: ${axiosLike.message || "unknown transport error"}`; + } + + if (e.message) { + return e.message; + } + + return "Response not valid"; +} diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/restApiUtils/helpers/RestAPIActivateUtils.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/restApiUtils/helpers/RestAPIActivateUtils.java index 2d40fa2107d8..9a5a6c2ed55b 100644 --- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/restApiUtils/helpers/RestAPIActivateUtils.java +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/restApiUtils/helpers/RestAPIActivateUtils.java @@ -19,6 +19,7 @@ import io.jsonwebtoken.security.Keys; import io.micrometer.observation.ObservationRegistry; import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatusCode; @@ -51,6 +52,7 @@ import static org.apache.commons.lang3.StringUtils.isNotEmpty; import static org.springframework.util.CollectionUtils.isEmpty; +@Slf4j @NoArgsConstructor public class RestAPIActivateUtils { @@ -146,7 +148,8 @@ public Mono triggerApiCall( result.setBody(objectMapper.readTree(jsonBody)); responseDataType = ResponseDataType.JSON; } catch (IOException e) { - System.out.println("Unable to parse response JSON. Setting response body as string."); + log.warn( + "Response declared Content-Type application/json but body is not valid JSON. Falling back to string representation."); String bodyString = new String(body, StandardCharsets.UTF_8); result.setBody(bodyString.trim()); diff --git a/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/plugins/RestApiPlugin.java b/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/plugins/RestApiPlugin.java index 152d881aee8f..9afdc3d66f67 100644 --- a/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/plugins/RestApiPlugin.java +++ b/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/plugins/RestApiPlugin.java @@ -226,10 +226,11 @@ public Mono executeCommon( errorResult.setRequest(requestCaptureFilter.populateRequestFields( actionExecutionRequest, isBodySentWithApiRequest, datasourceConfiguration)); errorResult.setIsExecutionSuccess(false); - log.debug(String.format( - "An error has occurred while trying to run the API query for url: %s, path: %s", - datasourceConfiguration.getUrl(), actionConfiguration.getPath())); - error.printStackTrace(); + log.error( + "REST API execution failed for url: {}, path: {}", + datasourceConfiguration.getUrl(), + actionConfiguration.getPath(), + error); if (!(error instanceof AppsmithPluginException)) { error = new AppsmithPluginException( RestApiPluginError.API_EXECUTION_FAILED, diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ActionExecutionSolutionCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ActionExecutionSolutionCEImpl.java index 6f24533717e5..815750895b08 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ActionExecutionSolutionCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ActionExecutionSolutionCEImpl.java @@ -391,7 +391,7 @@ public Mono executeAction( return actionExecutionResultMono .zipWith(editorConfigLabelMapMono, (result, labelMap) -> { if (TRUE.equals(executeActionDTO.getViewMode())) { - result.setRequest(null); + sanitizeRequestForViewMode(result); } else if (result.getRequest() != null && result.getRequest().getRequestParams() != null) { transformRequestParams(result, labelMap); @@ -870,9 +870,11 @@ protected Mono verifyDatasourceAndMakeRequest( } else if (error instanceof StaleConnectionException e) { return new AppsmithPluginException(AppsmithPluginError.STALE_CONNECTION_ERROR, e.getMessage()); } else { - log.debug( - "{}: In the action execution error mode.", - Thread.currentThread().getName(), + log.warn( + "Action execution failed for action '{}' (id: {}): {}", + actionDTO.getName(), + actionDTO.getId(), + error.getMessage(), error); return error; } @@ -881,6 +883,11 @@ protected Mono verifyDatasourceAndMakeRequest( protected Function> executionExceptionHandler(ActionDTO actionDTO) { return error -> { + log.warn( + "Handling execution error for action '{}' (id: {}): {}", + actionDTO.getName(), + actionDTO.getId(), + error.getMessage()); ActionExecutionResult result = new ActionExecutionResult(); result.setErrorInfo(error); result.setIsExecutionSuccess(false); @@ -1093,6 +1100,23 @@ private ActionExecutionResult addDataTypesAndSetSuggestedWidget(ActionExecutionR return result; } + /** + * In view/published mode, strip potentially sensitive fields from the request object + * (headers, body, URL, params) while retaining safe metadata (action ID, timestamp, + * HTTP method) so users can correlate which action failed and when. + */ + void sanitizeRequestForViewMode(ActionExecutionResult result) { + ActionExecutionRequest original = result.getRequest(); + if (original == null) { + return; + } + ActionExecutionRequest sanitized = new ActionExecutionRequest(); + sanitized.setActionId(original.getActionId()); + sanitized.setRequestedAt(original.getRequestedAt()); + sanitized.setHttpMethod(original.getHttpMethod()); + result.setRequest(sanitized); + } + /** * Since we're loading the application and other details from DB *only* for analytics, we check if analytics is * active before making the call to DB. diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ce/ActionExecutionSolutionCEImplTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ce/ActionExecutionSolutionCEImplTest.java index d5e077703ffa..e3a0fd1a6132 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ce/ActionExecutionSolutionCEImplTest.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ce/ActionExecutionSolutionCEImplTest.java @@ -463,4 +463,44 @@ public void testEnrichExecutionParams_withBlobReference_performsSubstitutionCorr }) .verifyComplete(); } + + @Test + void sanitizeRequestForViewMode_retainsSafeFieldsAndStripsSensitiveOnes() { + var original = new com.appsmith.external.models.ActionExecutionRequest(); + original.setActionId("action-123"); + original.setRequestedAt(java.time.Instant.parse("2026-04-30T12:00:00Z")); + original.setHttpMethod(HttpMethod.POST); + original.setUrl("https://internal-api.example.com/secret?key=abc"); + original.setBody("{\"password\": \"s3cret\"}"); + original.setHeaders(Map.of("Authorization", "Bearer token")); + original.setRequestParams(List.of("sensitive-param")); + + var result = new ActionExecutionResult(); + result.setRequest(original); + + actionExecutionSolution.sanitizeRequestForViewMode(result); + + var sanitized = result.getRequest(); + assertNotNull(sanitized); + assertEquals("action-123", sanitized.getActionId()); + assertEquals(java.time.Instant.parse("2026-04-30T12:00:00Z"), sanitized.getRequestedAt()); + assertEquals(HttpMethod.POST, sanitized.getHttpMethod()); + + // Sensitive fields must be stripped + assertEquals(null, sanitized.getUrl()); + assertEquals(null, sanitized.getBody()); + assertEquals(null, sanitized.getHeaders()); + assertEquals(null, sanitized.getRequestParams()); + assertEquals(null, sanitized.getExecutionParameters()); + } + + @Test + void sanitizeRequestForViewMode_handlesNullRequest() { + var result = new ActionExecutionResult(); + result.setRequest(null); + + actionExecutionSolution.sanitizeRequestForViewMode(result); + + assertEquals(null, result.getRequest()); + } } From f695136634b6cec5d617c53ee24f849f6acee801 Mon Sep 17 00:00:00 2001 From: subrata71 Date: Fri, 1 May 2026 13:35:29 +0600 Subject: [PATCH 2/3] fix(actions): drop datasource URL from error log to avoid leaking query-param secrets Keep httpMethod + path (safe metadata); drop datasourceConfiguration.getUrl() which could contain API keys in query parameters. --- .../src/main/java/com/external/plugins/RestApiPlugin.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/plugins/RestApiPlugin.java b/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/plugins/RestApiPlugin.java index 9afdc3d66f67..847a45ed799e 100644 --- a/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/plugins/RestApiPlugin.java +++ b/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/plugins/RestApiPlugin.java @@ -227,8 +227,8 @@ public Mono executeCommon( actionExecutionRequest, isBodySentWithApiRequest, datasourceConfiguration)); errorResult.setIsExecutionSuccess(false); log.error( - "REST API execution failed for url: {}, path: {}", - datasourceConfiguration.getUrl(), + "REST API execution failed for method: {}, path: {}", + actionExecutionRequest.getHttpMethod(), actionConfiguration.getPath(), error); if (!(error instanceof AppsmithPluginException)) { From ce142c582025df62282f43fb953b0cf820f396db Mon Sep 17 00:00:00 2001 From: subrata71 Date: Fri, 1 May 2026 14:44:48 +0600 Subject: [PATCH 3/3] fix(actions): use debug log level for upstream API failures, keep warn for unexpected errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Action execution failures are typically caused by the customer's upstream API (timeout, connection reset, invalid JSON), not by Appsmith itself. Logging these at warn/error creates noise in production and can trigger false alerts. - RestApiPlugin: log.error → log.debug (upstream REST failures) - executionExceptionMapper: log.warn → log.debug (duplicated by handler) - RestAPIActivateUtils: log.warn → log.debug (content-type mismatch) - executionExceptionHandler: kept at log.warn (rare, genuinely unexpected) Self-hosted operators who want to monitor upstream failure rates can lower the log level to DEBUG — making this opt-in rather than default. --- .../helpers/restApiUtils/helpers/RestAPIActivateUtils.java | 2 +- .../src/main/java/com/external/plugins/RestApiPlugin.java | 2 +- .../server/solutions/ce/ActionExecutionSolutionCEImpl.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/restApiUtils/helpers/RestAPIActivateUtils.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/restApiUtils/helpers/RestAPIActivateUtils.java index 9a5a6c2ed55b..184c651c2769 100644 --- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/restApiUtils/helpers/RestAPIActivateUtils.java +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/restApiUtils/helpers/RestAPIActivateUtils.java @@ -148,7 +148,7 @@ public Mono triggerApiCall( result.setBody(objectMapper.readTree(jsonBody)); responseDataType = ResponseDataType.JSON; } catch (IOException e) { - log.warn( + log.debug( "Response declared Content-Type application/json but body is not valid JSON. Falling back to string representation."); String bodyString = new String(body, StandardCharsets.UTF_8); result.setBody(bodyString.trim()); diff --git a/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/plugins/RestApiPlugin.java b/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/plugins/RestApiPlugin.java index 847a45ed799e..188d2faa574c 100644 --- a/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/plugins/RestApiPlugin.java +++ b/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/plugins/RestApiPlugin.java @@ -226,7 +226,7 @@ public Mono executeCommon( errorResult.setRequest(requestCaptureFilter.populateRequestFields( actionExecutionRequest, isBodySentWithApiRequest, datasourceConfiguration)); errorResult.setIsExecutionSuccess(false); - log.error( + log.debug( "REST API execution failed for method: {}, path: {}", actionExecutionRequest.getHttpMethod(), actionConfiguration.getPath(), diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ActionExecutionSolutionCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ActionExecutionSolutionCEImpl.java index 815750895b08..b156e6b55f82 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ActionExecutionSolutionCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ActionExecutionSolutionCEImpl.java @@ -870,7 +870,7 @@ protected Mono verifyDatasourceAndMakeRequest( } else if (error instanceof StaleConnectionException e) { return new AppsmithPluginException(AppsmithPluginError.STALE_CONNECTION_ERROR, e.getMessage()); } else { - log.warn( + log.debug( "Action execution failed for action '{}' (id: {}): {}", actionDTO.getName(), actionDTO.getId(),