This is an automated email from the ASF dual-hosted git repository. robertlazarski pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/axis-axis2-java-core.git
commit e2db339418c6c4b1922950a8cbe812d729bfedf3 Author: Robert Lazarski <[email protected]> AuthorDate: Tue May 5 03:06:36 2026 -1000 AXIS2-6104 Bring Moshi JSON-RPC receivers to feature parity with Gson The Moshi rpc/ package (JsonRpcMessageReceiver, JsonInOnlyRPCMessageReceiver, JsonUtils) now has identical error handling to the Gson equivalents: - JsonRpcFaultException detection: unwraps InvocationTargetException, checks for JsonRpcFaultException, sets HTTP status + structured Axis2JsonErrorResponse on the outgoing MessageContext - createSecureFault: new method in moshi JsonUtils with the same CWE-209-safe opaque errorRef pattern (both 1-arg and 4-arg overloads) - ITE unwrap: all catch blocks now log the root cause, not the wrapper The shared classes (Axis2JsonErrorResponse, JsonRpcFaultException, PaginatedResponse, PaginationRequest) live in gson.rpc and are referenced by both implementations — they contain no Gson-specific imports and are serialization-library-agnostic POJOs. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> --- .../moshi/rpc/JsonInOnlyRPCMessageReceiver.java | 31 ++++++------ .../json/moshi/rpc/JsonRpcMessageReceiver.java | 55 +++++++++++++++------- .../org/apache/axis2/json/moshi/rpc/JsonUtils.java | 51 ++++++++++++++++++++ 3 files changed, 105 insertions(+), 32 deletions(-) diff --git a/modules/json/src/org/apache/axis2/json/moshi/rpc/JsonInOnlyRPCMessageReceiver.java b/modules/json/src/org/apache/axis2/json/moshi/rpc/JsonInOnlyRPCMessageReceiver.java index 4514906a8c..a46430f11e 100644 --- a/modules/json/src/org/apache/axis2/json/moshi/rpc/JsonInOnlyRPCMessageReceiver.java +++ b/modules/json/src/org/apache/axis2/json/moshi/rpc/JsonInOnlyRPCMessageReceiver.java @@ -35,7 +35,7 @@ import java.lang.reflect.Method; public class JsonInOnlyRPCMessageReceiver extends RPCInOnlyMessageReceiver { private static final Log log = LogFactory.getLog(JsonInOnlyRPCMessageReceiver.class); - + @Override public void invokeBusinessLogic(MessageContext inMessage) throws AxisFault { Object tempObj = inMessage.getProperty(JsonConstant.IS_JSON_STREAM); @@ -70,7 +70,6 @@ public class JsonInOnlyRPCMessageReceiver extends RPCInOnlyMessageReceiver { } public void invokeService(JsonReader jsonReader, Object serviceObj, String operation_name, String enableJSONOnly) throws AxisFault { - String msg; Class implClass = serviceObj.getClass(); Method[] allMethods = implClass.getDeclaredMethods(); Method method = JsonUtils.getOpMethod(operation_name, allMethods); @@ -78,22 +77,22 @@ public class JsonInOnlyRPCMessageReceiver extends RPCInOnlyMessageReceiver { try { int paramCount = paramClasses.length; JsonUtils.invokeServiceClass(jsonReader, serviceObj, method, paramClasses, paramCount, enableJSONOnly); - } catch (IllegalAccessException e) { - msg = "Does not have access to " + - "the definition of the specified class, field, method or constructor"; - log.error(msg, e); - throw AxisFault.makeFault(e); - } catch (InvocationTargetException e) { - msg = "Exception occurred while trying to invoke service method " + - (method != null ? method.getName() : "null"); - log.error(msg, e); - throw AxisFault.makeFault(e); + // Unwrap the ITE to log the real cause, not the reflection wrapper. + Throwable cause = e.getCause(); + Exception rootCause; + if (cause instanceof Exception) { + rootCause = (Exception) cause; + } else if (cause != null) { + rootCause = new RuntimeException("Service threw non-Exception Throwable", cause); + } else { + rootCause = e; + } + throw JsonUtils.createSecureFault(rootCause, operation_name, false); + } catch (IllegalAccessException e) { + throw JsonUtils.createSecureFault(e, operation_name, false); } catch (IOException e) { - msg = "Exception occur while encording or " + - "access to the input string at the JsonRpcMessageReceiver"; - log.error(msg, e); - throw AxisFault.makeFault(e); + throw JsonUtils.createSecureFault(e, operation_name, true); } } } diff --git a/modules/json/src/org/apache/axis2/json/moshi/rpc/JsonRpcMessageReceiver.java b/modules/json/src/org/apache/axis2/json/moshi/rpc/JsonRpcMessageReceiver.java index 0c74df2010..2e6fed4a04 100644 --- a/modules/json/src/org/apache/axis2/json/moshi/rpc/JsonRpcMessageReceiver.java +++ b/modules/json/src/org/apache/axis2/json/moshi/rpc/JsonRpcMessageReceiver.java @@ -20,8 +20,11 @@ package org.apache.axis2.json.moshi.rpc; import com.squareup.moshi.JsonReader; import org.apache.axis2.AxisFault; +import org.apache.axis2.Constants; import org.apache.axis2.context.MessageContext; import org.apache.axis2.description.AxisOperation; +import org.apache.axis2.json.gson.rpc.Axis2JsonErrorResponse; +import org.apache.axis2.json.gson.rpc.JsonRpcFaultException; import org.apache.axis2.json.moshi.MoshiXMLStreamReader; import org.apache.axis2.json.factory.JsonConstant; import org.apache.axis2.rpc.receivers.RPCMessageReceiver; @@ -35,7 +38,7 @@ import java.lang.reflect.Method; public class JsonRpcMessageReceiver extends RPCMessageReceiver { private static final Log log = LogFactory.getLog(RPCMessageReceiver.class); - + @Override public void invokeBusinessLogic(MessageContext inMessage, MessageContext outMessage) throws AxisFault { Object tempObj = inMessage.getProperty(JsonConstant.IS_JSON_STREAM); @@ -69,7 +72,6 @@ public class JsonRpcMessageReceiver extends RPCMessageReceiver { } public void invokeService(JsonReader jsonReader, Object serviceObj, String operation_name, MessageContext outMes, String enableJSONOnly) throws AxisFault { - String msg; Class implClass = serviceObj.getClass(); Method[] allMethods = implClass.getDeclaredMethods(); Method method = JsonUtils.getOpMethod(operation_name, allMethods); @@ -82,22 +84,43 @@ public class JsonRpcMessageReceiver extends RPCMessageReceiver { outMes.setProperty(JsonConstant.RETURN_OBJECT, retObj); outMes.setProperty(JsonConstant.RETURN_TYPE, method.getReturnType()); - } catch (IllegalAccessException e) { - msg = "Does not have access to " + - "the definition of the specified class, field, method or constructor"; - log.error(msg, e); - throw AxisFault.makeFault(e); - } catch (InvocationTargetException e) { - msg = "Exception occurred while trying to invoke service method " + - (method != null ? method.getName() : "null"); - log.error(msg, e); - throw AxisFault.makeFault(e); + // Method.invoke() wraps any exception thrown by the service method + // in InvocationTargetException. Unwrap to inspect the real cause. + Throwable cause = e.getCause(); + + if (cause instanceof JsonRpcFaultException) { + // ── Structured error path ──────────────────────────────────── + // Service explicitly signaled a typed error (e.g. validation 422). + // Set the HTTP status code and put the error response as RETURN_OBJECT + // so the Moshi formatter serializes it as a normal JSON body — NOT + // through the SOAP fault path. + JsonRpcFaultException fault = (JsonRpcFaultException) cause; + Axis2JsonErrorResponse errorResponse = fault.getErrorResponse(); + log.warn("[errorRef=" + errorResponse.getErrorRef() + "] " + + errorResponse.getError() + " in operation '" + operation_name + + "': " + errorResponse.getMessage()); + outMes.setProperty(Constants.HTTP_RESPONSE_STATE, + String.valueOf(fault.getHttpStatusCode())); + outMes.setProperty(JsonConstant.RETURN_OBJECT, errorResponse); + outMes.setProperty(JsonConstant.RETURN_TYPE, Axis2JsonErrorResponse.class); + } else { + // ── Opaque error path ──────────────────────────────────────── + // Unexpected exception — create a CWE-209-safe AxisFault. + Exception rootCause; + if (cause instanceof Exception) { + rootCause = (Exception) cause; + } else if (cause != null) { + rootCause = new RuntimeException("Service threw non-Exception Throwable", cause); + } else { + rootCause = e; + } + throw JsonUtils.createSecureFault(rootCause, operation_name, false, outMes); + } + } catch (IllegalAccessException e) { + throw JsonUtils.createSecureFault(e, operation_name, false, outMes); } catch (IOException e) { - msg = "Exception occur while encording or " + - "access to the input string at the JsonRpcMessageReceiver"; - log.error(msg, e); - throw AxisFault.makeFault(e); + throw JsonUtils.createSecureFault(e, operation_name, true, outMes); } } } diff --git a/modules/json/src/org/apache/axis2/json/moshi/rpc/JsonUtils.java b/modules/json/src/org/apache/axis2/json/moshi/rpc/JsonUtils.java index ca3d94c9c8..9db4673ece 100644 --- a/modules/json/src/org/apache/axis2/json/moshi/rpc/JsonUtils.java +++ b/modules/json/src/org/apache/axis2/json/moshi/rpc/JsonUtils.java @@ -28,6 +28,12 @@ import com.squareup.moshi.JsonWriter; import com.squareup.moshi.Moshi; import com.squareup.moshi.adapters.Rfc3339DateJsonAdapter; +import org.apache.axis2.AxisFault; +import org.apache.axis2.Constants; +import org.apache.axis2.context.MessageContext; +import org.apache.axis2.json.factory.JsonConstant; +import org.apache.axis2.json.gson.rpc.Axis2JsonErrorResponse; + import java.io.IOException; import java.lang.annotation.Annotation; import java.lang.reflect.InvocationTargetException; @@ -36,6 +42,7 @@ import java.lang.reflect.Type; import java.util.Date; import java.util.Map; import java.util.Set; +import java.util.UUID; import jakarta.annotation.Nullable; @@ -147,4 +154,48 @@ public class JsonUtils { return null; } + /** + * Build a secure {@link AxisFault} for any unexpected failure in the Moshi JSON-RPC + * message receivers. Mirrors the Gson equivalent in + * {@link org.apache.axis2.json.gson.rpc.JsonUtils#createSecureFault}. + * + * <p>The full context is logged server-side under an opaque correlation ID; + * only {@code "Bad Request [errorRef=<uuid>]"} or + * {@code "Internal Server Error [errorRef=<uuid>]"} is returned to the caller. + * This prevents information disclosure (CWE-209). + */ + static AxisFault createSecureFault(Exception e, String operationName, boolean isParsingError) { + return createSecureFault(e, operationName, isParsingError, null); + } + + /** + * Build a secure {@link AxisFault} and, when an outgoing {@link MessageContext} + * is available, also set the structured {@link Axis2JsonErrorResponse} as the + * return object with the appropriate HTTP status code. + */ + static AxisFault createSecureFault(Exception e, String operationName, boolean isParsingError, + MessageContext outMessage) { + String errorRef = UUID.randomUUID().toString(); + String opDisplay = operationName != null ? operationName : "<unknown>"; + Axis2JsonErrorResponse errorResponse; + int httpStatus; + if (isParsingError) { + log.error("[errorRef=" + errorRef + "] Bad Request parsing JSON-RPC body " + + "for operation '" + opDisplay + "': " + e.getMessage(), e); + errorResponse = Axis2JsonErrorResponse.badRequest(errorRef); + httpStatus = 400; + } else { + log.error("[errorRef=" + errorRef + "] Internal error invoking operation '" + + opDisplay + "': " + e.getMessage(), e); + errorResponse = Axis2JsonErrorResponse.internalError(errorRef); + httpStatus = 500; + } + if (outMessage != null) { + outMessage.setProperty(Constants.HTTP_RESPONSE_STATE, String.valueOf(httpStatus)); + outMessage.setProperty(JsonConstant.RETURN_OBJECT, errorResponse); + outMessage.setProperty(JsonConstant.RETURN_TYPE, Axis2JsonErrorResponse.class); + } + return new AxisFault(errorResponse.getMessage()); + } + }
