-
Notifications
You must be signed in to change notification settings - Fork 628
Exception handling in REST APIs
Handling errors are essential in almost any scenario. An important task we should consider is to let the client know what went wrong and give him/her a direction to solve the issue. So, from the implementation PoV for any feature, it is an important task to think where the errors would come and propagate the errors to the client in a proper manner.
In this wiki, we are discussing an easier way to do it after solving a few problems we had earlier.
We already had some parts of this implementation in our C5 based codebase. This is an improved version of it.
A typical request/response flow involving the REST API looks like below:
Client --- (request) --> REST API Implementation (1) --- (request) --> API Mgt Core Impl (2)
Client <-- (respone) --- REST API Implementation (3) <-- (respone) --- API Mgt Core Impl (2)
An error can happen at any point in the above flow.
For an error happening at the (1) and (3) can be easily handled because it is at the REST API layer.
But when it comes to the errors happening at the API Mgt Core Impl (2) the exception handling code starts to become ugly.
See this example for handling different failure cases when updating an API:
The APIM core layer throws a general APIManagementException with an error message about the error and the REST API layer has to differentiate that using that error message.
This becomes a bit ugly and a repetitive task.
@Override
public Response apisApiIdPut(String apiId, APIDTO body, String ifMatch, MessageContext messageContext) {
...
API apiToUpdate = APIMappingUtil.fromDTOtoAPI(body, apiIdentifier.getProviderName());
...
apiProvider.manageAPI(apiToUpdate);
...
...
} catch (APIManagementException e) {
//Auth failure occurs when cross tenant accessing APIs. Sends 404, since we don't need
// to expose the existence of the resource
if (RestApiUtil.isDueToResourceNotFound(e) || RestApiUtil.isDueToAuthorizationFailure(e)) {
RestApiUtil.handleResourceNotFoundError(RestApiConstants.RESOURCE_API, apiId, e, log);
} else if (isAuthorizationFailure(e)) {
RestApiUtil.handleAuthorizationFailure("Authorization failure while updating API : " + apiId, e, log);
} else {
String errorMessage = "Error while updating API : " + apiId;
RestApiUtil.handleInternalServerError(errorMessage, e, log);
}
} catch (FaultGatewaysException e) {
String errorMessage = "Error while updating API : " + apiId;
RestApiUtil.handleInternalServerError(errorMessage, e, log);
}
Consider the below simple scenario.
- The user creates a normal REST API:
POST /apis
- Tries to download its WSDL.
GET /apis/{id}/wsdl
But, there is no WSDL present in the API so we need to provide an error to the user. But, if we are not writing any specific code to validate this case specifically, we only detect this at the core implementation layer when trying to fetch the WSDL from the registry and we figure out that the WSDL doesn't exist.
if (registry.resourceExists(wsdlResourcePath)) {
Resource resource = registry.get(wsdlResourcePath);
return new ResourceFile(resource.getContentStream(), resource.getMediaType());
} else {
// handle the exception
}
- Define an
ExceptionCode
inExceptionCodes.java
in a way that clearly describes the error.
//WSDL related codes
...
NO_WSDL_AVAILABLE_FOR_API(900684, "WSDL Not Found", 404, "No WSDL Available for the API %s:%s"),
An ExceptionCode gets 4 parameters:
-
errorCode
: A unique code which represents the error. Make sure there are no conflicts with any other error code which is already defined. -
errorMessage
: Title of the error message -
httpErrorCode
: Mapped HTTP Status code for the error -
errorDescription
: Description of the error message
- Create an exception with the above
ExceptionCode
and throw to the upper layers.
if (registry.resourceExists(wsdlResourcePath)) {
Resource resource = registry.get(wsdlResourcePath);
return new ResourceFile(resource.getContentStream(), resource.getMediaType());
} else {
throw new APIManagementException("No WSDL found for the API: " + apiId,
ExceptionCodes.from(ExceptionCodes.NO_WSDL_AVAILABLE_FOR_API, apiId.getApiName(),
apiId.getVersion()));
}
Note:
You can use ExceptionCodes.from()
method to pass the templated parameters defined in the ExceptionCode. Otherwise, just pass the ExceptionCode to the APIManagementException instance.
eg: throw new APIManagementException("Error message to log", ExceptionCodes.EXCEPTION_CODE_FOR_ERROR)
- Throw the Exception to the upper layer even from the REST API implementation. ***
@Override
public Response getWSDLOfAPI(String apiId, String ifNoneMatch, MessageContext messageContext)
throws APIManagementException {
APIProvider apiProvider = RestApiUtil.getLoggedInUserProvider();
String tenantDomain = RestApiUtil.getLoggedInUserTenantDomain();
APIIdentifier apiIdentifier = APIMappingUtil.getAPIIdentifierFromUUID(apiId, tenantDomain);
// This will throw the APIManagementException when the WSDL doesn't exist for the API.
// But, the exception is not caught at this method and thrown to the upper layers.
ResourceFile getWSDLResponse = apiProvider.getWSDL(apiIdentifier);
return RestApiUtil.getResponseFromResourceFile(apiIdentifier.toString(), getWSDLResponse);
}
*** In v1.0 REST APIs, we allow throwing APIManagementException
to even higher layer. Note throws APIManagementException
at the method signature.
That's it!
Try invoking the API and see whether you are getting the proper error response.
GET https://localhost:9443/api/am/publisher/v1.0/apis/0a6d997f-b2c6-46cd-b8da-f097f38df5ce-43fe-a1db-b5820271065b/wsdl HTTP/1.1
Authorization: Bearer f82832e9-19da-3694-bded-8d86330548d2
Host: localhost:9443
HTTP/1.1 404
Date: Mon, 23 Sep 2019 09:54:51 GMT
Content-Type: application/json
Server: WSO2 Carbon Server
{
"code": 900684,
"message": "WSDL Not Found",
"description": "No WSDL Available for the API PizzaShackAPI:1.0.0",
"moreInfo": "",
"error": []
}
Server logs:
[2019-09-23 15:32:36,435] ERROR - GlobalThrowableMapper A defined exception has been captured and mapped to an HTTP response by the global exception mapper
org.wso2.carbon.apimgt.api.APIManagementException: No WSDL found for the API: admin-PizzaShackAPI-1.0.0
at org.wso2.carbon.apimgt.impl.AbstractAPIManager.getWSDL_aroundBody58(AbstractAPIManager.java:1073) ~[org.wso2.carbon.apimgt.impl_6.5.122.SNAPSHOT.jar:?]
at org.wso2.carbon.apimgt.impl.AbstractAPIManager.getWSDL(AbstractAPIManager.java:1041) ~[org.wso2.carbon.apimgt.impl_6.5.122.SNAPSHOT.jar:?]
at org.wso2.carbon.apimgt.impl.UserAwareAPIProvider.getWSDL_aroundBody8(UserAwareAPIProvider.java:94) ~[org.wso2.carbon.apimgt.impl_6.5.122.SNAPSHOT.jar:?]
at org.wso2.carbon.apimgt.impl.UserAwareAPIProvider.getWSDL(UserAwareAPIProvider.java:92) ~[org.wso2.carbon.apimgt.impl_6.5.122.SNAPSHOT.jar:?]
The REST APIs are configured with an ExceptionMapper
GlobalThrowableMapper.java which can trap exceptions thrown from the REST API Implementation layer and map that to an HTTP Response.
GlobalThrowableMapper.java
is the central point which all the exceptions are handled at the REST API level before responding to the client. It will first of all log the exception to make sure no exception is swallowed.
It has a logic to filter exceptions of type APIManagementException
and convert that to an HTTP Response using the ExceptionCode it includes.
The mapping would be as follows:
EXCEPTION_CODE_FOR_ERROR($errorCode, $errorMessage, $httpStatusCode, $description),
HTTP/1.1 $httpStatusCode
Date: Mon, 23 Sep 2019 09:54:51 GMT
Content-Type: application/json
Server: WSO2 Carbon Server
{
"code": $errorCode,
"message": $errorMessage,
"description": $description,
"moreInfo": "",
"error": []
}
The ExceptionMapper is configured in beans.xml
.
<jaxrs:providers>
...
<bean class="org.wso2.carbon.apimgt.rest.api.util.exception.GlobalThrowableMapper" />
</jaxrs:providers>
We can also use this as an alternative for RestAPIUtil.handleXXXRequest()
.
Instead of:
RestApiUtil.handleBadRequest(
"Action '" + action + "' is not allowed. Allowed actions are " + Arrays
.toString(nextAllowedStates), log);
Use:
// define exception code in `ExceptionCodes.java`
INVALID_LIFECYCLE_ACTION(900883, "Invalid Lifecycle Action", 400, "Allowed actions are %s"),
@Override
public Response apisChangeLifecyclePost(String action, String apiId, String lifecycleChecklist,
String ifMatch, MessageContext messageContext) throws APIManagementException {
...
String[] nextAllowedStates = (String[]) apiLCData.get(APIConstants.LC_NEXT_STATES);
if (!ArrayUtils.contains(nextAllowedStates, action)) {
// throw the exception in the REST API layer.
throw new APIManagementException(ExceptionCodes.from(ExceptionCodes.INVALID_LIFECYCLE_ACTION, Arrays
.toString(nextAllowedStates)));
}
...
}