Skip to content

Exception handling in REST APIs

Tanya Madurapperuma edited this page Oct 7, 2019 · 3 revisions

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.

The Problem:

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);
        }

Solving this ..

Consider the below simple scenario.

  1. The user creates a normal REST API: POST /apis
  2. 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
                }

Procedure

  1. Define an ExceptionCode in ExceptionCodes.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
  1. 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)

  1. 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:?]

How does this work?

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.

Copy of REST API Arch

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>

Using this for validation in REST API implementation layer

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)));
     }
     ...
}