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

sergehuber pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/unomi.git


The following commit(s) were added to refs/heads/master by this push:
     new 3c6f936bf UNOMI-934/935: Return 400 for invalid rule/segment create 
(Phase 2 PR 4) (#787)
3c6f936bf is described below

commit 3c6f936bfdc07412889348ea2b6f8f36293cde98
Author: Serge Huber <[email protected]>
AuthorDate: Mon Jun 29 11:52:50 2026 +0200

    UNOMI-934/935: Return 400 for invalid rule/segment create (Phase 2 PR 4) 
(#787)
    
    Merge REST create validation
---
 .../exception/AbstractRestExceptionMapper.java     |  9 ++++
 .../BadSegmentConditionExceptionMapper.java        | 50 ++++++++++++++++++++++
 .../exception/IllegalArgumentExceptionMapper.java  | 49 +++++++++++++++++++++
 .../InternalServerErrorExceptionMapper.java        | 14 +++---
 .../rest/exception/RuntimeExceptionMapper.java     | 10 +++--
 .../rest/exception/RestExceptionMapperTest.java    | 45 +++++++++++++++++++
 6 files changed, 167 insertions(+), 10 deletions(-)

diff --git 
a/rest/src/main/java/org/apache/unomi/rest/exception/AbstractRestExceptionMapper.java
 
b/rest/src/main/java/org/apache/unomi/rest/exception/AbstractRestExceptionMapper.java
index 5f2f55045..73e316c7a 100644
--- 
a/rest/src/main/java/org/apache/unomi/rest/exception/AbstractRestExceptionMapper.java
+++ 
b/rest/src/main/java/org/apache/unomi/rest/exception/AbstractRestExceptionMapper.java
@@ -18,6 +18,7 @@ package org.apache.unomi.rest.exception;
 
 import com.fasterxml.jackson.core.JsonParseException;
 import com.fasterxml.jackson.databind.JsonMappingException;
+import org.apache.unomi.api.exceptions.BadSegmentConditionException;
 import org.apache.cxf.jaxrs.utils.JAXRSUtils;
 import org.apache.cxf.message.Message;
 import org.slf4j.Logger;
@@ -69,6 +70,14 @@ public abstract class AbstractRestExceptionMapper {
         return rootCause instanceof JsonMappingException || rootCause 
instanceof JsonParseException;
     }
 
+    /**
+     * @return {@code true} when the throwable represents invalid client input 
rejected by domain
+     * validation (e.g. rule or segment condition checks), rather than a 
server fault.
+     */
+    protected boolean isClientValidationError(Throwable throwable) {
+        return throwable instanceof IllegalArgumentException || throwable 
instanceof BadSegmentConditionException;
+    }
+
     protected Throwable getRootCause(Throwable throwable) {
         if (throwable == null) {
             return null;
diff --git 
a/rest/src/main/java/org/apache/unomi/rest/exception/BadSegmentConditionExceptionMapper.java
 
b/rest/src/main/java/org/apache/unomi/rest/exception/BadSegmentConditionExceptionMapper.java
new file mode 100644
index 000000000..a0c5783bb
--- /dev/null
+++ 
b/rest/src/main/java/org/apache/unomi/rest/exception/BadSegmentConditionExceptionMapper.java
@@ -0,0 +1,50 @@
+/*
+ * 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.unomi.rest.exception;
+
+import org.apache.unomi.api.exceptions.BadSegmentConditionException;
+import org.osgi.service.component.annotations.Component;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+/**
+ * Maps {@link BadSegmentConditionException} (invalid segment condition on 
{@code POST /cxs/segments})
+ * to a {@code 400 Bad Request} instead of a {@code 500 Internal Server Error}.
+ */
+@Provider
+@Component(service = ExceptionMapper.class)
+public class BadSegmentConditionExceptionMapper extends 
AbstractRestExceptionMapper
+        implements ExceptionMapper<BadSegmentConditionException> {
+
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(BadSegmentConditionExceptionMapper.class.getName());
+
+    @Override
+    public Response toResponse(BadSegmentConditionException exception) {
+        String requestContext = buildRequestContext();
+        String errorMessage = 
LogSanitizer.forLogging(messageOrType(exception));
+
+        LOGGER.warn("Bad request on {} - Invalid segment condition: {} (Set 
BadSegmentConditionExceptionMapper to debug to get the full stacktrace)",
+                requestContext, errorMessage);
+        LOGGER.debug("Full bad segment condition exception details for 
request: {}", requestContext, exception);
+
+        return badRequestResponse();
+    }
+}
diff --git 
a/rest/src/main/java/org/apache/unomi/rest/exception/IllegalArgumentExceptionMapper.java
 
b/rest/src/main/java/org/apache/unomi/rest/exception/IllegalArgumentExceptionMapper.java
new file mode 100644
index 000000000..b5cb3f2ba
--- /dev/null
+++ 
b/rest/src/main/java/org/apache/unomi/rest/exception/IllegalArgumentExceptionMapper.java
@@ -0,0 +1,49 @@
+/*
+ * 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.unomi.rest.exception;
+
+import org.osgi.service.component.annotations.Component;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+/**
+ * Maps {@link IllegalArgumentException} (e.g. invalid rule condition on 
{@code POST /cxs/rules})
+ * to a {@code 400 Bad Request} instead of a {@code 500 Internal Server Error}.
+ */
+@Provider
+@Component(service = ExceptionMapper.class)
+public class IllegalArgumentExceptionMapper extends AbstractRestExceptionMapper
+        implements ExceptionMapper<IllegalArgumentException> {
+
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(IllegalArgumentExceptionMapper.class.getName());
+
+    @Override
+    public Response toResponse(IllegalArgumentException exception) {
+        String requestContext = buildRequestContext();
+        String errorMessage = 
LogSanitizer.forLogging(messageOrType(exception));
+
+        LOGGER.warn("Bad request on {} - Invalid argument: {} (Set 
IllegalArgumentExceptionMapper to debug to get the full stacktrace)",
+                requestContext, errorMessage);
+        LOGGER.debug("Full illegal argument exception details for request: 
{}", requestContext, exception);
+
+        return badRequestResponse();
+    }
+}
diff --git 
a/rest/src/main/java/org/apache/unomi/rest/exception/InternalServerErrorExceptionMapper.java
 
b/rest/src/main/java/org/apache/unomi/rest/exception/InternalServerErrorExceptionMapper.java
index 42c4d437e..a500ab093 100644
--- 
a/rest/src/main/java/org/apache/unomi/rest/exception/InternalServerErrorExceptionMapper.java
+++ 
b/rest/src/main/java/org/apache/unomi/rest/exception/InternalServerErrorExceptionMapper.java
@@ -27,8 +27,9 @@ import javax.ws.rs.ext.Provider;
 
 /**
  * Maps {@link InternalServerErrorException}. When the underlying cause is a 
Jackson deserialization
- * failure (a wrapped client error) the response is downgraded to a {@code 400 
Bad Request};
- * otherwise it remains a {@code 500 Internal Server Error} with detailed, 
sanitized logging.
+ * failure or a domain validation error (a wrapped client error) the response 
is downgraded to a
+ * {@code 400 Bad Request}; otherwise it remains a {@code 500 Internal Server 
Error} with detailed,
+ * sanitized logging.
  */
 @Provider
 @Component(service = ExceptionMapper.class)
@@ -42,12 +43,13 @@ public class InternalServerErrorExceptionMapper extends 
AbstractRestExceptionMap
         String requestContext = buildRequestContext();
         Throwable rootCause = getRootCause(exception);
 
-        // A wrapped JSON deserialization failure is really a client error -> 
400 Bad Request.
-        if (isJsonDeserializationError(rootCause)) {
+        // A wrapped JSON deserialization failure or domain validation error 
is really a client
+        // error -> 400 Bad Request.
+        if (isJsonDeserializationError(rootCause) || 
isClientValidationError(rootCause)) {
             String errorMessage = 
LogSanitizer.forLogging(messageOrType(rootCause));
-            LOGGER.warn("Bad request on {} - JSON deserialization error: {} 
(Set InternalServerErrorExceptionMapper to debug to get the full stacktrace)",
+            LOGGER.warn("Bad request on {} - Root cause: {} (Set 
InternalServerErrorExceptionMapper to debug to get the full stacktrace)",
                     requestContext, errorMessage);
-            LOGGER.debug("Full JSON mapping exception details for request: 
{}", requestContext, exception);
+            LOGGER.debug("Full exception details for request: {}", 
requestContext, exception);
             return badRequestResponse();
         }
 
diff --git 
a/rest/src/main/java/org/apache/unomi/rest/exception/RuntimeExceptionMapper.java
 
b/rest/src/main/java/org/apache/unomi/rest/exception/RuntimeExceptionMapper.java
index a87cfbcc8..4c0b0c26b 100644
--- 
a/rest/src/main/java/org/apache/unomi/rest/exception/RuntimeExceptionMapper.java
+++ 
b/rest/src/main/java/org/apache/unomi/rest/exception/RuntimeExceptionMapper.java
@@ -39,9 +39,11 @@ public class RuntimeExceptionMapper extends 
AbstractRestExceptionMapper implemen
                 ? rootCause.getMessage()
                 : (exception.getMessage() != null ? exception.getMessage() : 
""));
 
-        // Client errors (deserialization) are logged at WARN; genuine server 
faults at ERROR.
-        boolean isDeserializationError = isJsonDeserializationError(rootCause);
-        if (isDeserializationError) {
+        // Client errors (deserialization or domain validation) are logged at 
WARN; genuine server faults at ERROR.
+        boolean isClientError = isJsonDeserializationError(rootCause)
+                || isClientValidationError(exception)
+                || isClientValidationError(rootCause);
+        if (isClientError) {
             LOGGER.warn(
                     "Bad request on {} - Root cause: {} - {} (Set 
RuntimeExceptionMapper to debug to get the full stacktrace)",
                     requestContext, rootCauseClassName, rootCauseMessage);
@@ -52,6 +54,6 @@ public class RuntimeExceptionMapper extends 
AbstractRestExceptionMapper implemen
         }
         LOGGER.debug("Full exception details for request: {}", requestContext, 
exception);
 
-        return isDeserializationError ? badRequestResponse() : 
internalServerErrorResponse();
+        return isClientError ? badRequestResponse() : 
internalServerErrorResponse();
     }
 }
diff --git 
a/rest/src/test/java/org/apache/unomi/rest/exception/RestExceptionMapperTest.java
 
b/rest/src/test/java/org/apache/unomi/rest/exception/RestExceptionMapperTest.java
index 4ca8ce885..1306f17e6 100644
--- 
a/rest/src/test/java/org/apache/unomi/rest/exception/RestExceptionMapperTest.java
+++ 
b/rest/src/test/java/org/apache/unomi/rest/exception/RestExceptionMapperTest.java
@@ -18,6 +18,7 @@ package org.apache.unomi.rest.exception;
 
 import com.fasterxml.jackson.core.JsonParseException;
 import com.fasterxml.jackson.databind.JsonMappingException;
+import org.apache.unomi.api.exceptions.BadSegmentConditionException;
 import org.junit.jupiter.api.Test;
 
 import javax.ws.rs.InternalServerErrorException;
@@ -86,6 +87,50 @@ class RestExceptionMapperTest {
         assertErrorResponse(response, 500, "internalServerError");
     }
 
+    @Test
+    void illegalArgumentException_mapsToBadRequest() {
+        Response response = new IllegalArgumentExceptionMapper()
+                .toResponse(new IllegalArgumentException("Invalid rule 
condition:\n- missing type"));
+        assertErrorResponse(response, 400, "badRequest");
+    }
+
+    @Test
+    void badSegmentConditionException_mapsToBadRequest() {
+        Response response = new BadSegmentConditionExceptionMapper()
+                .toResponse(new BadSegmentConditionException("Invalid segment 
condition:\n- missing parameter"));
+        assertErrorResponse(response, 400, "badRequest");
+    }
+
+    @Test
+    void runtimeException_withIllegalArgumentCause_mapsToBadRequest() {
+        RuntimeException exception = new RuntimeException(new 
IllegalArgumentException("invalid rule"));
+        Response response = new RuntimeExceptionMapper().toResponse(exception);
+        assertErrorResponse(response, 400, "badRequest");
+    }
+
+    @Test
+    void runtimeException_withBadSegmentConditionCause_mapsToBadRequest() {
+        RuntimeException exception = new RuntimeException(new 
BadSegmentConditionException("invalid segment"));
+        Response response = new RuntimeExceptionMapper().toResponse(exception);
+        assertErrorResponse(response, 400, "badRequest");
+    }
+
+    @Test
+    void internalServerError_withIllegalArgumentCause_mapsToBadRequest() {
+        InternalServerErrorException exception = new 
InternalServerErrorException("wrapped",
+                new IllegalArgumentException("invalid rule"));
+        Response response = new 
InternalServerErrorExceptionMapper().toResponse(exception);
+        assertErrorResponse(response, 400, "badRequest");
+    }
+
+    @Test
+    void internalServerError_withBadSegmentConditionCause_mapsToBadRequest() {
+        InternalServerErrorException exception = new 
InternalServerErrorException("wrapped",
+                new BadSegmentConditionException("invalid segment"));
+        Response response = new 
InternalServerErrorExceptionMapper().toResponse(exception);
+        assertErrorResponse(response, 400, "badRequest");
+    }
+
     private static void assertErrorResponse(Response response, int 
expectedStatus, String expectedErrorMessage) {
         assertEquals(expectedStatus, response.getStatus());
         Object entity = response.getEntity();

Reply via email to