API Business Services - Customize error messages
2022.2
In the creation or modification services, when the instance is saved, triggers can raise functional exceptions, in the form of noheto.BusinessException. This type of exception is encapsulated in an API exception to be converted to an error in the response to the service invocation, in a general form described here.
In debug mode, on developpement environment, the BusinessException message is visible as well as the complete stack trace. But in normal mode, in production environment, the messages are purged of detailed or technical information that could be used to penetrate the system. As it is not possible to know in general whether the BusinessException message contains sensitive information, it is only accessible in debug mode.
Sometimes it may be necessary to provide more specific information to an API consumer for various reasons, without providing technical or detailed information, and that is what this extension component allows. For that, you need to implement com.noheto.restapi.APIBusinessExceptionInterceptorAdapter
.
methods
public boolean accept(String type, String actionName, String objectName, IObjectStructureReadOnly structure)
This method allows a quick selection of a component according to the service that triggers the exception. If it returns true, the intercept method of this component will be called. This method is called once to determine the components to be used to handle the exceptions raised by the service, and will not be called again for this service.public void intercept(BusinessExceptionContext context)
This method is called to determine whether to decorate the error with information for the service invocator that informs about the reasons for the BusinessException.
context
It provides the description of the context of the exception and allows to decorate the error with information about the BusinessException
void setSubMessage(String subMessage)
Allows you to indicate the decoration message. Use a vague message, with no sensitive or detailed, let alone technical, information. Hackers may purposely send requests that cause errors in order to collect information that can give them clues about the system and possibly allow them to attempt other requests for malicious actions. Keep in mind that the caller is supposed to know the parameters they are sending in the request.void setSubCode(String subCode)
Allows you to specify an error code that allows automated processing on the invoker's side.void setLogLevel(org.apache.log4j.Level level)
By default the exceptions generated to produce the API error message are logged at the DEBUG level. It is possible to change the log level via this method.Throwable getThrowable()
Returns the exception to be caught. Usually this is anoheto.BusinessException
, but it can be other exceptions that may be raised by the save instruction. The API already handles a number of generic errors, but may ask the extension component to handle some errors that are not detected by the API, such aswsnoheto.error.ILocalizedException
for example.CTSurfer getSurfer()
Returns the surfer who invoked the service.Locale getLocale()
Returns the request locale (not the surfer locale).HttpServletRequest getRequest()
Returns the requestObject getProperty(String property)
Gets a context property of the exception triggering. Depending on the service, the properties can be different. Here is the list:
Name | Description | Constant in the class BusinessExceptionContext | |
---|---|---|---|
type | the type of service | PROP_TYPE | |
| LEGACY_CREATE_UPDATE | legacy create or update services | TYPE_LEGACY_CREATE_UPDATE |
| DAM | dam create or update services | TYPE_DAM |
| DATA | data create or update services | TYPE_DATA |
| PROFIL | profil update services | TYPE_PROFIL |
| MASSIMPORT | massimport create or update services | TYPE_MASSIMPORT |
actionName | The name of action | PROP_ACTION_NAME | |
objectName | The name of object | PROP_OBJECT_NAME | |
objectStructure | The structure object (class | PROP_OBJECT_STRUCTURE | |
resourceObject | The object in its state after the invocation of the "save" method (class | PROP_OBJECT | |
serviceId | ID of service (not available for legacy services) | PROP_SERVICE_ID | |
serviceType | Type of service (not available for legacy services) | PROP_SERVICE_TYPE | |
endpointType | Type of endpoint (not available for legacy services) | PROP_ENDPOINT_TYPE | |
contextName | Context name (not available for legacy services) | PROP_CONTEXT_NAME | |
requestMode | Request mode (not available for legacy services) | PROP_REQUEST_MODE | |
finalMode | Final mode (not available for legacy services) | PROP_FINAL_MODE | |
resourceName | Name of resource (not available for legacy services) | PROP_RESOURCE_NAME |
Set<String> getProperties()
Gives the list of all existing properties for this interception.String getType()
Returns the type of invocation. Corresponds to a call ofgetProperty(BusinessExceptionContext.PROP_TYPE)
.String getActionName()
Returns the name of the action. Corresponds to a call ofgetProperty(BusinessExceptionContext.PROP_ACTION_NAME)
.String getObjectName()
Returns the name of the object. Corresponds to a call ofgetProperty(BusinessExceptionContext.PROP_OBJECT_NAME)
.IObjectStructureReadOnly getObjectStructure()
Returns the object that represents the structure of the object. Corresponds to a call ofgetProperty(BusinessExceptionContext.PROP_OBJECT_STRUCTURE)
.void setData(String key, Object data)
Allows to store data in the context. When there are several components that correspond to the service, we call them one after the other until we find one that specifies a submessage. It is possible to communicate between these calls via the data stored in the context (to avoid for example decoding the same information several times)<T> T getData(String key)
Returns a store data
Example
For this example, we use a simple object, named testbusinessexception
:
And we configure a legacy creation and modification action named A_BUSINESSEXCEPTIONTEST
:
{
"objectname": "testbusinessexception",
"description": "Test interception Business Exception",
"update": true,
"create": true,
"patch": true,
"workflow": false,
"props": {
"name": {
"i18n": false
}
},
"reportprops": [
"name"
]
}
The URI we will invoke allows us to create a new instance with a name that will trigger the exception when it is equal to "exception".
POST https://example.host.com/api/json/create/A_BUSINESSEXCEPTIONTEST?prop_name=exception
In debug mode, this URI will generate a response like this (The stacktrace has been deliberately removed to reduce the example), in debug mode, on a developpement environnement:
{
"message": "A functional error has occurred: Error test, create testbusinessexception/0. ",
"error": "400/7",
"errtag": "f7",
"errorSupportDetails": [
{
"value": "Wedia support email: support@wedia-group.com.",
"key": "WEDIA_ERROR_SUPPORT",
"params": [
"support@wedia-group.com"
]
}
],
"error_uri": "http://localhost:9080/enginepkg/api/rest/errors#400/7",
"errorWithContact": false,
"cause": {
"exception": "com.noheto.apirest.core.exceptions.BusinessException",
"message": "Business exception: Error test, create testbusinessexception/0",
"stacktrace": [
],
"cause": {
"exception": "noheto.BusinessException",
"message": "Test erreur, create testbusinessexception/0",
"stacktrace": [],
"cause": null
}
},
"version": "2.3",
"status": 400,
"time": 246
}
In normal mode (debug=false), on developpement environment, the response will be:
{
"message": "A functional error has occurred: Error test, create testbusinessexception/0. ",
"error": "400/7",
"errtag": "f6",
"errorSupportDetails": [
{
"value": "Wedia support email: support@wedia-group.com.",
"key": "WEDIA_ERROR_SUPPORT",
"params": [
"support@wedia-group.com"
]
}
],
"error_uri": "http://localhost:9080/enginepkg/api/rest/errors#400/7",
"errorWithContact": false,
"version": "2.3"
}
In the both, we can see the business exception original message. In the second version, the stacktrace is not present.
On a production environment the response will be:
{
"message": "A functional error has occurred.",
"error": "400/19",
"errtag": "ec",
"errorSupportDetails": [
{
"value": "Wedia support email: support@wedia-group.com.",
"key": "WEDIA_ERROR_SUPPORT",
"params": [
"support@wedia-group.com"
]
}
],
"error_uri": "http://localhost:9080/enginepkg/api/rest/errors#400/19",
"errorWithContact": false,
"version": "2.3"
}
The business exception original message is not visible.
Setting the business service
First, here is the trigger code that will generate the exception:
import java.util.Objects;
import com.noheto.extensions.interfaces.services.AbstractObjectTriggerBusinessService;
import noheto.BusinessException;
import wsnoheto.engine.CTSurfer;
import wsnoheto.engine.IObjectReadOnly;
import wsnoheto.engine.IObjectStructureReadOnly;
import wsnoheto.engine.IObjectTableReadOnly;
import wsnoheto.engine.IObjectWritable;
public class TriggerTestBusinessException extends AbstractObjectTriggerBusinessService {
@Override
public boolean executeOnUpdate(IObjectStructureReadOnly structure, IObjectTableReadOnly config) throws Throwable {
return accept(structure);
}
@Override
public boolean executeOnInsert(IObjectStructureReadOnly structure, IObjectTableReadOnly config) throws Throwable {
return accept(structure);
}
private boolean accept(IObjectStructureReadOnly structure) {
return TestBusinessExceptionAdapter.OBJECT_NAME.equals( structure.getObjectType().toString() );
}
@Override
public void onUpdateStart(IObjectWritable oActiveObject, IObjectReadOnly oPreviousObject, CTSurfer oSurfer)
throws Throwable {
if ( test(oActiveObject) ) {
throw createBusinessException(oSurfer,TestBusinessExceptionAdapter.BUSINESS_EXCEPTION_UPDATE_KEY, oActiveObject.toString());
}
}
@Override
public void onInsertStart(IObjectWritable oActiveObject, CTSurfer oSurfer) throws Throwable {
if ( test(oActiveObject) ) {
throw createBusinessException(oSurfer,TestBusinessExceptionAdapter.BUSINESS_EXCEPTION_CREATE_KEY, oActiveObject.toString());
}
}
private boolean test(IObjectWritable oActiveObject) throws Throwable {
String value = oActiveObject.getProperty("name");
return Objects.equals(value,"exception");
}
private BusinessException createBusinessException(CTSurfer surfer, String key, Object...params) {
return BusinessException.create(key).setParams(params).setBundle(plugin.getBundle(surfer.getLocale()));
}
}
Next, here is the implementation of the interceptor:
import java.util.Locale;
import com.noheto.restapi.APIBusinessExceptionInterceptorAdapter;
import com.noheto.restapi.BusinessExceptionContext;
import noheto.BusinessException;
import wsnoheto.engine.IObjectStructureReadOnly;
public class TestBusinessExceptionAdapter extends APIBusinessExceptionInterceptorAdapter {
public static final String OBJECT_NAME = "testbusinessexception";
public static final String ACTION_NAME = "A_BUSINESSEXCEPTIONTEST";
public static final String BUSINESS_EXCEPTION_UPDATE_KEY = "BUSINESS_EXCEPTION_TEST_UPDATE";
public static final String BUSINESS_EXCEPTION_CREATE_KEY = "BUSINESS_EXCEPTION_TEST_CREATE";
@Override
public boolean accept(String type, String actionName, String objectName, IObjectStructureReadOnly structure) {
boolean accepted;
switch(type) {
case BusinessExceptionContext.TYPE_LEGACY_CREATE_UPDATE: // service legacy create/update
accepted = TestBusinessExceptionAdapter.ACTION_NAME.equals(actionName) && TestBusinessExceptionAdapter.OBJECT_NAME.equalsIgnoreCase(objectName);
break;
default:
accepted = false;
}
return accepted;
}
@Override
public void intercept(BusinessExceptionContext context) {
Throwable throwable = context.getThrowable();
if ( throwable instanceof BusinessException ) {
switch( ((BusinessException) throwable).getKey() ) {
case TestBusinessExceptionAdapter.BUSINESS_EXCEPTION_CREATE_KEY:
case TestBusinessExceptionAdapter.BUSINESS_EXCEPTION_UPDATE_KEY:
intercept(context, "BUSINESS_EXCEPTION_MESSAGE" /* sub message key */, "INVALID_DATA" /* sub error code */);
break;
default:
// ignore
}
}
}
private void intercept(BusinessExceptionContext context, String key, String code) {
context.setSubMessage( getMessage( getLocale(context), key ) );
context.setSubCode( code );
}
private String getMessage(Locale locale, String key) {
return getPlugin().getBundle(locale).getFormatedString(key);
}
}
Here is the localization resource bundle:
BUSINESS_EXCEPTION_TEST_UPDATE = Error test, update {0}
BUSINESS_EXCEPTION_TEST_CREATE = Error test, create {0}
BUSINESS_EXCEPTION_MESSAGE = Invalid data
In debug mode, on developpement environment, we get this response:
{
"message": "A functional error has occurred: Error test, create testbusinessexception/0.",
"submessage": "Invalid data",
"error": "400/19",
"subcode": "INVALID_DATA",
"errtag": "ec",
"errorSupportDetails": [
{
"value": "Wedia support email: support@wedia-group.com.",
"key": "WEDIA_ERROR_SUPPORT",
"params": [
"support@wedia-group.com"
]
}
],
"error_uri": "http://localhost:9080/enginepkg/api/rest/errors#400/19",
"errorWithContact": false,
"cause": {
"exception": "com.noheto.apirest.core.exceptions.WithSubMessageBusinessException",
"message": "Business exception: Error test, create testbusinessexception/0",
"stacktrace": [
],
"cause": {
"exception": "noheto.BusinessException",
"message": "Test erreur, create testbusinessexception/0",
"stacktrace": [],
"cause": null
}
},
"version": "2.3",
"status": 400,
"time": 334
}
In normal mode (debug=false), on development environment, we get:
{
"message": "A functional error has occurred: Error test, create testbusinessexception/0.",
"submessage": "Invalid data",
"error": "400/19",
"subcode": "INVALID_DATA",
"errtag": "ed",
"errorSupportDetails": [
{
"value": "Wedia support email: support@wedia-group.com.",
"key": "WEDIA_ERROR_SUPPORT",
"params": [
"support@wedia-group.com"
]
}
],
"error_uri": "http://localhost:9080/enginepkg/api/rest/errors#400/19",
"errorWithContact": false,
"version": "2.3"
}
Finally, on production environment, the response will be:
{
"message": "A functional error has occurred.",
"submessage": "Invalid data",
"error": "400/19",
"subcode": "INVALID_DATA",
"errtag": "ec",
"errorSupportDetails": [
{
"value": "Wedia support email: support@wedia-group.com.",
"key": "WEDIA_ERROR_SUPPORT",
"params": [
"support@wedia-group.com"
]
}
],
"error_uri": "http://localhost:9080/enginepkg/api/rest/errors#400/19",
"errorWithContact": false,
"version": "2.3"
}
Note the error code is not the same. The original encapsulation exception, code 400/7, has not been changed, for backward compatibility and documentation reasons. A new exception has been created with code 400/19.