Return custom error responses using Policy Fragment


Introduction

It’s a good practice to return a unform error response in case something occurs in your API’s that isn’t expected. I’m using the format Microsoft is describing in their API Guidelines: api-guidelines/Guidelines.md at master · microsoft/api-guidelines · GitHub. This is used in the backends of our APIM environment, and if not, the error messages are transformed to comply with this format in the APIM Policies.

An example of an error message using the format (simplified).

{
    "error": {
        "code": "ValidationErrors",
        "message": "There are validation errors.",
        "details": [
            {
                "target": "AppointmentEndDateTime",
                "message": "'Appointment End Date Time' must be greater than '03/03/2024 08:00:00'."
            },
            {
                "target": "LicensePlate",
                "message": "The length of 'License Plate' must be at least 6 characters. You entered 5 characters."
            }
        ]
    }
}

However, if an error occurs on APIM level, the error will be returned in a different format, for example:

{
    "statusCode": 401,
    "message": "Access denied due to missing subscription key. Make sure to include subscription key when making requests to an API."
}

or

{
    "statusCode": 500,
    "message": "Internal server error",
    "activityId": "9ea182e9-1826-4fe5-9696-92cf183f132c"
}

This will cause implementations to fail because of a different response body than documented and intended by your backend (or policy).
To solve this issue, you can implement a Policy Fragment into the on-error section of your Policy (on All API Level).


Create Policy Fragment

To create a policy fragment, to go the Azure Portal, Select your APIM instance and fo to ‘Policy fragments’ and hit ‘Create’

Provide a recognizable name, for example ‘DefaultErrorResponse’ and use the following Policy fragment:

<fragment>
	<choose>
		<when condition="@(context.Response.StatusCode >= 400)">
		<set-body template="none">@{
                    var errorResponseBody = new JObject();
                    errorResponseBody["error"] = new JObject();
                    errorResponseBody["error"]["code"] = context.LastError.Reason ?? "UndefinedError";
                    errorResponseBody["error"]["message"] = context.LastError.Message;
                    errorResponseBody["error"]["target"] = context.LastError.Source;
                    return errorResponseBody.ToString();
                }</set-body>
		</when>
	</choose>
</fragment>

In the example, we’re setting the requestbody based on an HTTP status code >= 400. You can also define different responsebody’s based on the StatusCode (e.g. HTTP 500 will return a different format).

I’m using the context.LastError object to get some information about the error message. Depending on your needs, you can adjust the format of the error response, add response-headers etc. For information about the available properties in the object, please check the Microsoft Documentation: Error handling in Azure API Management policies | Microsoft Learn

I’ve noticed that when an error occurs when using the ‘authentication-managed-identity’ policy, context.LastError.Reason is null. This could also occur in other scenario’s this this field is optional, Therefore a fallback should be implemented in the policy fragment.


Apply Policy Fragment in policy

To apply the error message to the on-error section in the APIM policy (in this example on the All API level:)

<policies>
    <inbound />
    <backend>
        <forward-request />
    </backend>
    <outbound />
    <on-error>
        <include-fragment fragment-id="DefaultErrorResponse" />
    </on-error>
</policies>

Result

The result of the error message before and after implementing the policy fragment:

Before:
{
    "statusCode": 401,
    "message": "Access denied due to missing subscription key. Make sure to include subscription key when making requests to an API."
}
After:
{
    "error": {
        "code": "SubscriptionKeyNotFound",
        "message": "Access denied due to missing subscription key. Make sure to include subscription key when making requests to an API.",
        "target": "authorization"
    }
}

You can decide to adjust the response format based on the response code (4xx responses with details in the message field and 5xx response without details in the message field) by adding an additional <when condition> in the policy fragment. You could also decide to include an unique correlationId for the request in either the HTTP header (e.g. ‘x-correlation-id’) or as part of the response body to easily track any errors reported by your users.