Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

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;
Expand Down Expand Up @@ -69,6 +70,14 @@ protected boolean isJsonDeserializationError(Throwable rootCause) {
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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@

/**
* 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)
Expand All @@ -42,12 +43,13 @@ public Response toResponse(InternalServerErrorException exception) {
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();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,11 @@ public Response toResponse(RuntimeException exception) {
? 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);
Expand All @@ -52,6 +54,6 @@ public Response toResponse(RuntimeException exception) {
}
LOGGER.debug("Full exception details for request: {}", requestContext, exception);

return isDeserializationError ? badRequestResponse() : internalServerErrorResponse();
return isClientError ? badRequestResponse() : internalServerErrorResponse();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

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;
Expand Down Expand Up @@ -86,6 +87,50 @@ void internalServerError_withNonJsonCause_mapsToInternalServerError() {
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();
Expand Down
Loading