This is an automated email from the ASF dual-hosted git repository.

Lukas-Finster pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/ofbiz-framework.git

commit aeef97110f01af3a0db2d2bb88c190541b0a9d7f
Author: Lukas Finster <[email protected]>
AuthorDate: Wed Aug 5 11:10:50 2026 +0200

    Implemented: Custom errorCodes independent from http status codes in
    rest-api component (OFBIZ-12580)
---
 framework/rest-api/servicedef/services.xml         |  7 ++++
 .../org/apache/ofbiz/ws/rs/response/Error.java     | 47 +++++++++++++++++-----
 .../rs/spi/impl/GenericServiceExceptionMapper.java |  8 ++--
 .../org/apache/ofbiz/ws/rs/util/RestApiUtil.java   | 45 ++++++++++++++++++++-
 .../apache/ofbiz/ws/rs/test/RestServicesTests.java | 11 +++++
 .../apache/ofbiz/ws/rs/test/RestTestServices.java  | 41 +++++++++++++++++++
 .../apache/ofbiz/ws/rs/util/RestApiUtilTest.java   |  4 +-
 .../org/apache/ofbiz/service/ModelService.java     |  1 +
 .../apache/ofbiz/service/ModelServiceReader.java   |  1 +
 9 files changed, 148 insertions(+), 17 deletions(-)

diff --git a/framework/rest-api/servicedef/services.xml 
b/framework/rest-api/servicedef/services.xml
index ed90202f09..0f91c1a509 100644
--- a/framework/rest-api/servicedef/services.xml
+++ b/framework/rest-api/servicedef/services.xml
@@ -41,4 +41,11 @@ under the License.
         <attribute name="Order" 
type="org.apache.ofbiz.ws.rs.examples.RestOrderExample" mode="IN" 
optional="false"/>
         <attribute name="orderId" type="String" mode="OUT" optional="true"/>
     </service>
+
+    <!-- TestServices -->
+    <service name="returnCustomErrorTest" engine="java"
+            location="org.apache.ofbiz.ws.rs.test.RestTestServices" 
invoke="returnCustomErrorTest">
+        <description>TestService that returns a custom errorCode</description>
+    </service>
+            
 </services>
diff --git 
a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/response/Error.java 
b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/response/Error.java
index 7ea073e4a2..edfb0d005f 100644
--- 
a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/response/Error.java
+++ 
b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/response/Error.java
@@ -34,8 +34,8 @@ public class Error {
     private String errorMessage;
     @JsonProperty("errorType")
     private String type;
-    @JsonProperty("errorDescription")
-    private String errorDesc;
+    private String errorCode;
+    private String errorDescription;
     private List<String> additionalErrors;
 
     /**
@@ -133,8 +133,19 @@ public class Error {
      * <p>These methods allow chaining to populate error metadata in a concise 
way.
      * Each method sets a field and returns the same {@code Error} 
instance.</p>
      */
-    public Error errorDesc(String errorDesc) {
-        this.setErrorDesc(errorDesc);
+    public Error errorDescription(String errorDescription) {
+        this.setErrorDescription(errorDescription);
+        return this;
+    }
+
+    /**
+     * Fluent builder methods for constructing an {@code Error} response.
+     *
+     * <p>These methods allow chaining to populate error metadata in a concise 
way.
+     * Each method sets a field and returns the same {@code Error} 
instance.</p>
+     */
+    public Error errorCode(String errorCode) {
+        this.setErrorCode(errorCode);
         return this;
     }
 
@@ -183,6 +194,24 @@ public class Error {
         this.statusDescription = statusDescription;
     }
 
+    /**
+     * Returns the errorCode
+     *
+     * @return errorCode
+     */
+    public String getErrorCode() {
+        return errorCode;
+    }
+
+    /**
+     * Sets the errorCode
+     *
+     * @param errorCode the errorCode to set
+     */
+    public void setErrorCode(String errorCode) {
+        this.errorCode = errorCode;
+    }
+
     /**
      * Returns the error message.
      *
@@ -224,17 +253,17 @@ public class Error {
      *
      * @return the error description
      */
-    public String getErrorDesc() {
-        return errorDesc;
+    public String getErrorDescription() {
+        return errorDescription;
     }
 
     /**
      * Sets the error description.
      *
-     * @param errorDesc the error description to set
+     * @param errorDescription the error description to set
      */
-    public void setErrorDesc(String errorDesc) {
-        this.errorDesc = errorDesc;
+    public void setErrorDescription(String errorDescription) {
+        this.errorDescription = errorDescription;
     }
 
 }
diff --git 
a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/spi/impl/GenericServiceExceptionMapper.java
 
b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/spi/impl/GenericServiceExceptionMapper.java
index 78988ad546..905678158b 100644
--- 
a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/spi/impl/GenericServiceExceptionMapper.java
+++ 
b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/spi/impl/GenericServiceExceptionMapper.java
@@ -90,7 +90,7 @@ public class GenericServiceExceptionMapper implements 
jakarta.ws.rs.ext.Exceptio
                     .statusCode(Response.Status.BAD_REQUEST.getStatusCode())
                     .description(Response.Status.BAD_REQUEST.getReasonPhrase())
                     .message(RestApiUtil.getErrorMessage(service, 
"GenericServiceValidationErrorMessage", request.getLocale()))
-                    .errorDesc((validationException.getMessage()))
+                    .errorDescription((validationException.getMessage()))
                     .additionalErrors(validationException.getMessageList());
             builder = 
Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON).entity(error);
         } else if (actualCause instanceof GenericNoSuchEntityException
@@ -99,7 +99,7 @@ public class GenericServiceExceptionMapper implements 
jakarta.ws.rs.ext.Exceptio
                     
.statusCode(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode())
                     
.description(Response.Status.INTERNAL_SERVER_ERROR.getReasonPhrase())
                     .message(RestApiUtil.getErrorMessage(service, 
"NoSuchEntityDefaultMessage", request.getLocale()))
-                    .errorDesc(ExceptionUtils.getRootCauseMessage(gse));
+                    .errorDescription(ExceptionUtils.getRootCauseMessage(gse));
             builder = 
Response.status(Response.Status.INTERNAL_SERVER_ERROR).type(MediaType.APPLICATION_JSON)
                     .entity(error);
         } else if (actualCause instanceof GenericEntityException) {
@@ -108,7 +108,7 @@ public class GenericServiceExceptionMapper implements 
jakarta.ws.rs.ext.Exceptio
                     
.description(ResponseStatus.Custom.UNPROCESSABLE_ENTITY.getReasonPhrase())
                     .message(RestApiUtil.getErrorMessage(service, 
"GenericServiceExecutionGenericEntityOperationErrorMessage",
                             request.getLocale()))
-                    .errorDesc(ExceptionUtils.getRootCauseMessage(gse));
+                    .errorDescription(ExceptionUtils.getRootCauseMessage(gse));
             builder = 
Response.status(ResponseStatus.Custom.UNPROCESSABLE_ENTITY).type(MediaType.APPLICATION_JSON)
                     .entity(error);
         } else {
@@ -117,7 +117,7 @@ public class GenericServiceExceptionMapper implements 
jakarta.ws.rs.ext.Exceptio
                     
.description(Response.Status.INTERNAL_SERVER_ERROR.getReasonPhrase())
                     .message(RestApiUtil.getErrorMessage(service, 
"GenericServiceExecutionGenericExceptionErrorMessage",
                             request.getLocale()))
-                    .errorDesc(ExceptionUtils.getRootCauseMessage(gse));
+                    .errorDescription(ExceptionUtils.getRootCauseMessage(gse));
             builder = 
Response.status(Response.Status.INTERNAL_SERVER_ERROR).type(MediaType.APPLICATION_JSON)
                     .entity(error);
         }
diff --git 
a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/util/RestApiUtil.java 
b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/util/RestApiUtil.java
index 22e14ffab7..d8907ab3eb 100644
--- 
a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/util/RestApiUtil.java
+++ 
b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/util/RestApiUtil.java
@@ -33,6 +33,7 @@ import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 
+import org.apache.ofbiz.base.util.UtilGenerics;
 import org.apache.ofbiz.base.util.UtilProperties;
 import org.apache.ofbiz.base.util.UtilValidate;
 import org.apache.ofbiz.service.ModelService;
@@ -40,6 +41,7 @@ import org.apache.ofbiz.ws.rs.core.ResponseStatus;
 import org.apache.ofbiz.ws.rs.response.Error;
 import org.apache.ofbiz.ws.rs.response.Success;
 
+import jakarta.ws.rs.core.Response.StatusType;
 import jakarta.ws.rs.core.MediaType;
 import jakarta.ws.rs.core.MultivaluedMap;
 import jakarta.ws.rs.core.Response;
@@ -47,6 +49,7 @@ import jakarta.ws.rs.core.Response.ResponseBuilder;
 
 public final class RestApiUtil {
 
+    public static final String RESPONSE_STATUS_KEY = "httpResponseStatus";
     private static final String DEFAULT_MSG_UI_LABEL_RESOURCE = "ApiUiLabels";
     private static final String QUERY_STRING_SEPARATOR = "&";
 
@@ -168,9 +171,19 @@ public final class RestApiUtil {
                 additionalErrorMessages.add(errorMessageList.get(i));
             }
         }
-        Error error = new 
Error().type("ServiceError").statusCode(ResponseStatus.Custom.UNPROCESSABLE_ENTITY.getStatusCode())
+        String errorCode = null;
+        if (!UtilValidate.isEmpty(result.get(ModelService.ERROR_CODE))) {
+            errorCode = result.get(ModelService.ERROR_CODE).toString();
+        }
+        StatusType status = extractResponseCode(result);
+        if (status == null) {
+            status = ResponseStatus.Custom.UNPROCESSABLE_ENTITY;
+        }
+
+        Error error = new 
Error().type("ServiceError").statusCode(status.getStatusCode())
                 
.description(ResponseStatus.Custom.UNPROCESSABLE_ENTITY.getReasonPhrase())
-                .message(getErrorMessage(service, 
"GenericServiceErrorMessage", locale)).errorDesc(errorMessage);
+                .message(getErrorMessage(service, 
"GenericServiceErrorMessage", locale))
+                .errorDescription(errorMessage).errorCode(errorCode);
         if (!additionalErrorMessages.isEmpty()) {
             error.setAdditionalErrors(additionalErrorMessages);
         }
@@ -624,4 +637,32 @@ public final class RestApiUtil {
             return value;
         }
     }
+
+    /**
+     * Extracts the http status code from the service result to override the 
default.
+     * The parameter is removed from the resultMap to not show in the 
resulting data.
+     * @param data
+     * @return
+     */
+    public static StatusType extractResponseCode(Object data) {
+        Map<String, ? extends Object> resultMap = UtilGenerics.<String, 
Object>checkMap(data, String.class, Object.class);
+        Integer statusCode = null;
+
+        try {
+            statusCode = (Integer) resultMap.get(RESPONSE_STATUS_KEY);
+        } catch (ClassCastException e) {
+            // do nothing
+        }
+
+        if (statusCode == null) {
+            return null;
+        }
+        resultMap.remove(RESPONSE_STATUS_KEY);
+
+        StatusType statusType = Response.Status.fromStatusCode(statusCode);
+        if (statusType == null) {
+            statusType = ResponseStatus.Custom.fromStatusCode(statusCode);
+        }
+        return statusType;
+    }
 }
diff --git 
a/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/test/RestServicesTests.java
 
b/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/test/RestServicesTests.java
index 17d9ec0b36..018d8f9ca5 100644
--- 
a/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/test/RestServicesTests.java
+++ 
b/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/test/RestServicesTests.java
@@ -30,6 +30,7 @@ import java.util.Map;
 
 import org.apache.ofbiz.base.util.UtilMisc;
 import org.apache.ofbiz.entity.GenericValue;
+import org.apache.ofbiz.service.ModelService;
 import org.apache.ofbiz.service.ServiceUtil;
 import org.apache.ofbiz.testtools.JunitJupiterTest;
 import org.apache.ofbiz.testtools.JupiterTestHelper;
@@ -229,4 +230,14 @@ public class RestServicesTests implements 
JupiterTestHelper {
         for (int i = 0; i < padding; i++) sb.append('=');
         return sb.toString();
     }
+
+    @Test
+    public void testReturnCustomErrorCode() throws Exception {
+        GenericValue adminLogin = getDelegator().findOne("UserLogin", 
UtilMisc.toMap("userLoginId", "admin"), false);
+        Map<String, Object> result = getDispatcher().runSync(
+                "returnCustomErrorTest",
+                UtilMisc.toMap("userLogin", (Object) adminLogin));
+        String errorCode = (String) result.get(ModelService.ERROR_CODE);
+        assertTrue(errorCode.equals("999"));
+    }
 }
diff --git 
a/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/test/RestTestServices.java
 
b/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/test/RestTestServices.java
new file mode 100644
index 0000000000..948c378bd8
--- /dev/null
+++ 
b/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/test/RestTestServices.java
@@ -0,0 +1,41 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ 
*******************************************************************************/
+package org.apache.ofbiz.ws.rs.test;
+
+import java.util.Map;
+
+import org.apache.ofbiz.service.DispatchContext;
+import org.apache.ofbiz.service.ModelService;
+import org.apache.ofbiz.service.ServiceUtil;
+
+public class RestTestServices {
+
+    /**
+     * Testservice returning a custom ErrorCode
+     *
+     * @param dctx
+     * @param context
+     * @return result
+     */
+    public static Map<String, Object> returnCustomErrorTest(DispatchContext 
dctx, Map<String, ? extends Object> context) {
+        Map<String, Object> result = ServiceUtil.returnError("Some error");
+        result.put(ModelService.ERROR_CODE, 999);
+        return result;
+    }
+}
diff --git 
a/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/util/RestApiUtilTest.java
 
b/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/util/RestApiUtilTest.java
index 679dc99574..d8d79c8296 100644
--- 
a/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/util/RestApiUtilTest.java
+++ 
b/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/util/RestApiUtilTest.java
@@ -73,7 +73,7 @@ public final class RestApiUtilTest {
 
         assertEquals(expected.getAdditionalErrors(), 
actual.getAdditionalErrors());
         assertEquals(expected.getClass(), actual.getClass());
-        assertEquals(expected.getErrorDesc(), actual.getErrorDesc());
+        assertEquals(expected.getErrorDescription(), 
actual.getErrorDescription());
         assertEquals(expected.getErrorMessage(), actual.getErrorMessage());
         assertEquals(expected.getStatusCode(), actual.getStatusCode());
         assertEquals(expected.getStatusDescription(), 
actual.getStatusDescription());
@@ -153,7 +153,7 @@ public final class RestApiUtilTest {
 
         Error error = (Error) response.getEntity();
 
-        assertEquals("errorOne", error.getErrorDesc());
+        assertEquals("errorOne", error.getErrorDescription());
         assertEquals(List.of("errorTwo", "errorThree"), 
error.getAdditionalErrors());
     }
 }
diff --git 
a/framework/service/src/main/java/org/apache/ofbiz/service/ModelService.java 
b/framework/service/src/main/java/org/apache/ofbiz/service/ModelService.java
index 8fb9e511fd..3dfc3bc1d0 100644
--- a/framework/service/src/main/java/org/apache/ofbiz/service/ModelService.java
+++ b/framework/service/src/main/java/org/apache/ofbiz/service/ModelService.java
@@ -111,6 +111,7 @@ public class ModelService extends AbstractMap<String, 
Object> implements Seriali
     public static final String RESPOND_SUCCESS = "success";
     public static final String RESPOND_ERROR = "error";
     public static final String RESPOND_FAIL = "fail";
+    public static final String ERROR_CODE = "errorCode";
     public static final String ERROR_MESSAGE = "errorMessage";
     public static final String ERROR_MESSAGE_LIST = "errorMessageList";
     public static final String ERROR_MESSAGE_MAP = "errorMessageMap";
diff --git 
a/framework/service/src/main/java/org/apache/ofbiz/service/ModelServiceReader.java
 
b/framework/service/src/main/java/org/apache/ofbiz/service/ModelServiceReader.java
index 604868eda2..3beec16fb4 100644
--- 
a/framework/service/src/main/java/org/apache/ofbiz/service/ModelServiceReader.java
+++ 
b/framework/service/src/main/java/org/apache/ofbiz/service/ModelServiceReader.java
@@ -471,6 +471,7 @@ public final class ModelServiceReader implements 
Serializable {
         }
 
         // Add the default optional parameters
+        service.addParam(createInternalParam(ModelService.ERROR_CODE, 
"String", ModelService.OUT_PARAM));
         service.addParam(createInternalParam(ModelService.RESPONSE_MESSAGE, 
"String", ModelService.OUT_PARAM));
         service.addParam(createInternalParam(ModelService.ERROR_MESSAGE, 
"String", ModelService.OUT_PARAM));
         service.addParam(createInternalParam(ModelService.ERROR_MESSAGE_LIST, 
"java.util.List", ModelService.OUT_PARAM));

Reply via email to