Exception handling in REST APIs - wso2/carbon-apimgt GitHub Wiki

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. A user tries to delete gateway environment
curl --location --request DELETE 'https://127.0.0.1:9443/api/am/admin/v4/environments/d7cf8523-9180-4255-84fa-6cb171c1f779' \
--header 'Authorization: Bearer 91a5e83c-a400-3583-83cd-9623f815e578'

But, if the given environmentId is not available in the db, 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 environment from the registry and we figure out that the environment doesn't exist.

            env = apiMgtDAO.getEnvironment(tenantDomain, uuid);
            if (env == null) {
                // Handle the exception
            }

Procedure

  1. Define an ExceptionCode in ExceptionCodes.java in a way that clearly describes the error.
   // Gateway related codes
   ...
   GATEWAY_ENVIRONMENT_NOT_FOUND(900506, "Gateway Environment not found", 404,
            "Gateway Environment with %s not found"),

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.
    @Override
    public Environment getEnvironment(String tenantDomain, String uuid) throws APIManagementException {
        Environment env = APIUtil.getReadOnlyEnvironments().get(uuid);
        if (env == null) {
            env = apiMgtDAO.getEnvironment(tenantDomain, uuid);
            if (env == null) {
                String errorMessage = String.format("Failed to retrieve Environment with UUID %s. Environment not found",
                        uuid);
                throw new APIMgtResourceNotFoundException(errorMessage, ExceptionCodes.from(
                        ExceptionCodes.GATEWAY_ENVIRONMENT_NOT_FOUND, String.format("UUID '%s'", uuid))
                );
            }
        }
        ...

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 boolean hasExistingDeployments(String tenantDomain, String uuid) throws APIManagementException {
        Environment existingEnv = getEnvironment(tenantDomain, uuid);
        return StringUtils.isNotEmpty(
                apiMgtDAO.getGatewayPolicyMappingByGatewayLabel(existingEnv.getDisplayName(), tenantDomain));
    }
    public Response environmentsEnvironmentIdDelete(String environmentId, MessageContext messageContext) throws APIManagementException {
            APIAdmin apiAdmin = new APIAdminImpl();
            //String tenantDomain = RestApiCommonUtil.getLoggedInUserTenantDomain();
            String organization = RestApiUtil.getValidatedOrganization(messageContext);
            if (apiAdmin.hasExistingDeployments(organization, environmentId)) {
                RestApiUtil.handleConflict("Cannot delete the environment with id: " + environmentId
                        + " as active gateway policy deployment exist", log);
            }
    ....

From version 1.0 to later REST API versions (2.0, 3.0, 4.0), 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.

curl --location --request DELETE 'https://127.0.0.1:9443/api/am/admin/v4/environments/d7cf8523-9180-4255-84fa-6cb171c1f779' \
--header 'Authorization: Bearer 91a5e83c-a400-3583-83cd-9623f815e578'

{
    "code": 900506,
    "message": "Gateway Environment not found",
    "description": "Gateway Environment with UUID 'd7cf8523-9180-4255-84fa-6cb171c1f779' not found",
    "moreInfo": "",
    "error": []
}

Server logs:

[2025-06-29 20:30:03,579] ERROR - GlobalThrowableMapper Failed to retrieve Environment with UUID d7cf8523-9180-4255-84fa-6cb171c1f779. Environment not found

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.

  • The GlobalThrowableMapper class is an implementation of the JAX-RS ExceptionMapper<Throwable> interface, which provides a centralized mechanism to handle all exceptions thrown by the REST API implementation layer and map them to appropriate HTTP responses.

  • This ensures that exceptions are not only logged but also converted into meaningful and consistent HTTP responses that can be sent back to the client.

  • It acts as the global exception mapper for the REST API layer. Any uncaught exception thrown from the REST API implementation is intercepted by this mapper.

    <init-param>
        <param-name>jaxrs.providers</param-name>
        <param-value>
            com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider,
            org.wso2.carbon.apimgt.rest.api.util.exception.GlobalThrowableMapper,
            org.apache.cxf.rs.security.cors.CrossOriginResourceSharingFilter(allowHeaders=Authorization allowHeaders=X-WSO2-Tenant allowHeaders=content-type exposeHeaders=Content-Disposition allowCredentials=true allowOrigins={systemProperties['rest.api.admin.allowed.origins']})
        </param-value>
    </init-param>

Copy of REST API Arch

In Summary, the exception is thrown from the exact point where it occurs and throw it to higher level. It is then mapped to an appropriate HTTP response using an ExceptionMapper, which converts it into a proper HTTP error response sent back to the client.

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)));
    }
    ...
}
⚠️ **GitHub.com Fallback** ⚠️