Skip to Content
Technical Articles
Author's profile photo Mike Zaschka

SAP API Management – Error handling with FaultRules, the DefaultFaultRule and the RaiseFault Policy (Part 2 of 2)

Introduction

In my first blog post about error handling with FaultRules, the DefaultFaultRule and the RaiseFault Policy I tried to give a short introduction into the various elements, that can be used to implement a custom error handling in SAP API Management.
In this post I will showcase an implementation pattern, that we at p36 have used in customer projects to enable a flexible and extendable custom error handling.

Error handling requirements

We implemented this pattern with a focus on the following requirements:

  • Return a standardized error response for all errors, that is aligned with the SAP Enterprise API Best Practices (which also many of our clients in the SAP space use as part of their API Guideline)
{
  "error": {
    "code": "<some internal code or text like btp.apmgmt.auth.MissingApiKey>",
    "message": "Missing APIKey header. Please provide an APIKey in the request header for authentication."
  }
}
  • Separate the content of an error (code and message) from the structure and don’t repeat the same structure in multiple places
  • Enable the capability to override Policy errors (e.g. to use a different http status code or message), but make reuse of the default messages for not overridden errors
  • Support custom errors via RaiseFault Policy as well as Policy errors
  • Return a valid response for different accept headers (e.g. application/json, application/xml)

Our error handling pattern

And this is how we implement the requirements:

Use FaultRules to handle Policy errors

We introduce a set of FaultRules in our flows (check my previous post on how to edit FaultRules in SAP API Management) for every Policy error, we want to change regarding the error message or http status code.
For every relevant Policy, we add a specific FaultRule, that checks if the Policy generally fails. Inside of the FaultRule, we then use the steps to handle specific errors.
The following code snippet shows an example, in which we want to modify some errors for a VerifyApiKey and one specific error for a Quota Policy:

<faultRules>
    <faultRule>
        <name>APIKeyErrors</name>
        <condition>(oauthV2.VK-VerifyAPIKey.failed = true)</condition>
        <steps>
            <step>
                <policy_name>AM-FaultRuleInvalidAPIKey</policy_name>
                <condition>(fault.name = "InvalidApiKey")</condition>
                <sequence>0</sequence>
            </step>
            <step>
                <policy_name>AM-FaultDeveloperStatusNotActive</policy_name>
                <condition>(fault.name = "DeveloperStatusNotActive")</condition>
                <sequence>0</sequence>
            </step>
            <step>
                <policy_name>AM-FaultRuleNoAPIKey</policy_name>
                <condition>(fault.name = "FailedToResolveAPIKey")</condition>
                <sequence>0</sequence>
            </step>
        </steps>
    </faultRule>
    <faultRule>
        <name>QuotaViolationErrors</name>
        <condition>(ratelimit.QT-RateLimit.failed = true)</condition>
        <steps>
            <step>
                <policy_name>AM-FaultRuleQuotaViolation</policy_name>
                <condition>(fault.name = "QuotaViolation")</condition>
                <sequence>0</sequence>
            </step>
        </steps>
    </faultRule>
</faultRules>

The FaultRule delegates the error handling to different AssignMessage Policies, that are part of the API Proxy and which define the custom error handling.

Use AssignMessage Policies and Variables to define the error handling

By using an AssignMessage Policy, it is possible to set parts of the response, as well as to assign Variables. In our fault handling-AssignMessage Policies, we use the default method (Set) to override the http status codes, the corresponding phrases and other response header information, but rely on custom Variables for setting the error code and message (custom.error.code and custom.error.message). By using Variables, we can stay unspecific about the returned payload, which we will define in a later step.
The following example shows the AM-FaultRuleQuotaViolation Policy, which is being triggered, if the QT-Ratelimit Quota Policy fails with the fault name QuotaViolation. The AssignMessage Policy then sets the http status to 429 Too many requests, adds some additional Retry-After header and modifies the code and message Variables to contain the customized error information.

<AssignMessage async="false" continueOnError="false" enabled="true" xmlns='http://www.sap.com/apimgmt'>
    <Set>
        <StatusCode>429</StatusCode>
        <ReasonPhrase>Too many requests</ReasonPhrase>
        <Headers>
            <Header name="Retry-After">{ratelimit.QT-RateLimit.expiry.time}</Header>
        </Headers>
    </Set>
    <AssignVariable>
        <Name>custom.error.code</Name>
        <Value>btp.apmgmt.quota.QuotaViolation</Value>
    </AssignVariable>
    <AssignVariable>
        <Name>custom.error.message</Name>
        <Value>The Quota limit has been reached. Please try again later.</Value>
    </AssignVariable>
    <IgnoreUnresolvedVariables>true</IgnoreUnresolvedVariables>
    <AssignTo createNew="false" transport="http" type="request"/>
</AssignMessage>

Always call the DefaultFaultRule to set the error response

Since in the FaultRules we only set some response headers and keep the payload information in Variables, there needs to be a place where the response content will be set. This is done as part of the DefaultFaultRule, which will be executed every time any kind of error occurs, because we set  alwaysEnforce to true.
By enforcing the DefaultFaultRule, we can use the steps and conditions to call a different AssignMessage Policy and set a specific response based on the accept header of the incoming request:

<defaultFaultRule>
    <name>defaultfaultRule</name>
    <alwaysEnforce>true</alwaysEnforce>
    <steps>
        <step>
            <policy_name>JS-FaultRuleSetErrorVariables</policy_name>
            <sequence>0</sequence>
        </step>
        <step>
            <policy_name>AM-FaultRuleToXML</policy_name>
            <condition>(request.header.accept = "application/xml")</condition>
            <sequence>1</sequence>
        </step>
            <step>
            <policy_name>AM-FaultRuleToJSON</policy_name>
            <condition>(request.header.accept = "application/json")</condition>
            <sequence>2</sequence>
        </step>
        <step>
            <policy_name>AM-FaultRuleToPlaintext</policy_name>
            <condition>(request.header.accept = null) or ((request.header.accept != "application/json") and (request.header.accept != "application/xml"))</condition>
            <sequence>3</sequence>
        </step>
    </steps>
</defaultFaultRule>

Use JavaScript to enable fallback scenarios

But before digging into the AssignMessage Policies that set the payload, we take a look at the JavaScript Policy that gets executed as a first step: Because the DefaultFaultRule gets executed every time an error occurs (even for one, that is not handled by a custom FaultRule), we need to implement the fallback behavior, that checks if the custom Variables have been set and if not, fill those with the default error message and a default error code:

var customErrorMessage = context.getVariable("custom.error.message");
if (!customErrorMessage) {
    var errorMessage = context.getVariable("error.message");
    context.setVariable("custom.error.message", errorMessage);
}

var customErrorCode = context.getVariable("custom.error.code");
if (!customErrorMessage) {
    context.setVariable("custom.error.code", "btp.apmgmt.InternalError");
}

This way we make sure, that the Variables will always be filled and can be safely used later.

Handle different accept headers by using different AssignMessage Policies

As shown above, the DefaultFaultRule calls different AssignMessage Policies based on the accept header of the incoming request. In those AssignMessage Policies, we just use the custom variables to set the payload, which then gets returned in the response.
The JSON error response (AM-FaultRuleToJSON) looks like this:

<AssignMessage async="false" continueOnError="false" enabled="true" xmlns='http://www.sap.com/apimgmt'>
    <Set>
        <Payload contentType="application/json" variablePrefix="@" variableSuffix="#">
		    {
              "error": {
                "code": "@custom.error.code#",
                "message": "@custom.error.message#"
              }
            }
        </Payload>
    </Set>
    <IgnoreUnresolvedVariables>true</IgnoreUnresolvedVariables>
    <AssignTo createNew="false" transport="http" type="request"/>
</AssignMessage>

Whereas the XML error response (AM-FaultRuleToXML) just places the variables in an XML payload structure:

<AssignMessage async="false" continueOnError="false" enabled="true" xmlns='http://www.sap.com/apimgmt'>
	<Set>
		<Payload contentType="application/xml" variablePrefix="@" variableSuffix="#">
			<error>
				<code>@custom.error.code#</code>
				<message>@custom.error.message#</message>
			</error>
		</Payload>
	</Set>
	<IgnoreUnresolvedVariables>true</IgnoreUnresolvedVariables>
	<AssignTo createNew="false" transport="http" type="request"/>
</AssignMessage>

What about Target Endpoint errors?

Since we enabled the DefaultFaultRule for every error that gets thrown, this will also have an impact on the Target Endpoint. If the Endpoint returns a non successful http code, the DefaultFaultRule will also be triggered and thus hide the original and probably valuable error message.
To overcome this problem, it is possible to specify http status codes, that should be interpreted as a successful response and not trigger the error handling in SAP API Management.
This can easily be done by setting the corresponding success.codes property:

Add custom errors

Up until now I mainly described the handling of Policy errors. But this pattern also works with custom errors raised by a RaiseFault Policy. The FaultResponse inside the RaiseFault Policy supports setting http codes and header information, as well as setting values for our custom variables. And because a RaiseFault Policy will also trigger the DefaultFaultRule, the errors will be handled in the same way as the Policy errors.
The automatically added defaultRaiseFaultPolicy could then potentially be modified like this:

<RaiseFault async="false" continueOnError="false" enabled="true" xmlns="http://www.sap.com/apimgmt">
    <FaultResponse>
        <AssignVariable>
            <Name>custom.error.code</Name>
            <Value>btp.apmgmt.MethodNotAllowed</Value>
        </AssignVariable>
        <AssignVariable>
            <Name>custom.error.message</Name>
            <Value>The ressource you tried to access does respond to the used http method.</Value>
        </AssignVariable>
        <Set>
	        <StatusCode>500</StatusCode>
			<ReasonPhrase>Method Not Allowed</ReasonPhrase>
        </Set>
    </FaultResponse>
	<IgnoreUnresolvedVariables>false</IgnoreUnresolvedVariables>
</RaiseFault>

Summary

By using this error handling pattern, we are able to keep many of the original fault handling mechanisms of SAP API Management in place, but have the possibility to modify specific errors and to introduce a custom response payload, even for different content types.
But since FaultRules and the DefaultFaultRule are not manageable in the API Portal and also not easily shareable via Policy Templates, the process of applying and modifying those for many API Proxies can be a little cumbersome. Nevertheless the value of a standardized and clean error handling outsells the little extra work by far.

That’s our approach, but I am eager to learn if there are alternatives out there. So in case you handle errors differently, please share your pattern in the comments below.

Assigned tags

      1 Comment
      You must be Logged on to comment or reply to a post.
      Author's profile photo Kevin Wang
      Kevin Wang

      Thanks for sharing, Mike.