A few months ago, I wrote two blog posts about How to get the Error Message with Logic App Try-Catch, which you can find it here:
- How to get the Error Message with Logic App Try-Catch (Part I)
- How to get the Error Message with Logic App Try-Catch (Part II) – Using an Azure Function
Of course, in this series of posts, we are addressing Logic App Consumption. We can actually implement the same strategy for Logic App Standard, but the APIs will be different – this is something that I will write about in the future.
Nevertheless, when I published the second part of this series, I mentioned that we could actually use a no code-low code approach using a Logic App to perform the same operation we were doing by code inside an Azure Function. And that got curiosity by some of my readers to question me if I was doing a thirty-part addressing that scenario. Well, it took some time, but here it is!
What we pretend to do here is to create a generic Logic App Consumption that can dynamically catch the actual error message and action inside a run from another Logic App. This means that we will discard generical errors like “An action failed. No dependent actions succeeded.” which don’t tell us anything about what really happened during the workflow, only that a subsequent child action failed, and dig deeper to find the real error behind that.
The Logic App will receive the same inputs of the Azure Function that we described in the previous post:
- Subscription Id;
- Resource name;
- Logic App name;
- Run id;
But in this case, in a JSON format using the Request > When a HTTP Request is received trigger.
{
"subscriptionId": "xxxxx",
"resourceGroup": "RG-DEMO-LASTD-MAIN",
"workflowName": "LA-catchError-POC",
"runId": "08585259279877955762218280603CU192"
}
Of course, to do this, after we add the trigger, we need to click on Use sample payload to generate schema.
And paste the above JSON sample for the editor to generate the JSON schema
The next step we are going to do is to invoke the Azure Logic App REST API in order to get the run history:
GET https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Logic/workflows/{workflowName}/runs/{runName}/actions?api-version=2016-06-01
Here is the link for the Microsoft documentation about is: Workflow Run Actions – List.
We are going to do that by creating a new HTTP action.
Of course, we need to:
- Specify the Method parameter to be GET.
- On the URL parameter, copy the URL described above and replace:
- {subscriptionId} by the token subscriptionId present in the trigger
- {resourceGroupName} by the token resourceGroup present in the trigger
- {workflowName} by the token workflowName present in the trigger
- {runName} by the token runId present in the trigger
- On the Headers parameter, add the following header:
- Content-Type: application/json
- On the Authentication type, we will use Managed identity to dynamically generate the OAuth token necessary to invoke the Azure Logic App REST API.
Managed identities in Azure Logic Apps are an authentication mechanism that allows the workflow to access other services without having the user define the credentials for those services inside the workflow actions.
Of course, to use it, we need to go to our Logic App resource and enable managed identity by:
- On your LogicApp, from the left menu, go to the Identity option present in the Settings group, and once there, on the Status property, click on On, this will generate an Object (principal) ID.
Later on, we will be setting the correct permissions.
Getting back to our Logic App, now that we have an action to invoke the Azure Logic App REST API to get the run history, we are going to use a Filter Array action to filter the action with the status equal to Failed like we describe in the first blog on this series:
- Select Add an action.
- In the search box, enter Filter array, and from the result panel, select the Data Operations, Filter array action
- And provide the following information:
- on the From property, place the following expression:
- body(‘Call_Logic_App_Rest_API_To_Get_Run_History’)?[‘value’]
- Note: that ‘Call_Logic_App_Rest_API_To_Get_Run_History‘ is the name of the previous HTTP action, so you need to adjust this value to your scenario
- on the condition property, on the left textbox, place the following expression:
- item()?[‘properties’]?[‘status’]
- Note: this is always equal
- Leave the operator as is equal to, and on the right textbox, place the following value:
- Failed
- on the From property, place the following expression:
So, in this last action, we will create an array object for all actions that contain the property status equal to Failed. However, that can also have the generic error that we need to discard to archive that we are going to create a For each action to travel that array and find the correct error:
As I mentioned in the last post, depending on the scenario and actions you are using, the error information may be in different places and structures of the JSON in the run history:
- Usually, we have inside the properties a structure call error that has the error message inside the message property: action[“properties”][“error”][“message”]
- But sometimes, this error structure doesn’t exist – the HTTP action is a good example of this – and we need to get uri information on the outputsLink structure: action[“properties”][“outputsLink”][“uri”], to invoke that URL to get to correct error message.
These are different behaviors that we need to handle inside our workflow. Therefore, we will add another Action, this time a Condition with the following configuration:
- Items(‘For_each_Filter_Array_Run_History’)[‘properties’]?[‘error’] is equal to null
As you see in the picture below.
What does this mean? It means that if inside the properties of the JSON, the object error is null (does not exist):
- Then the Logic App goes through the true side of the condition, and there we need to implement the logic to get and invoke the URI to get the error details
- Otherwise, If it is false and indeed the properties-error contains an object error, then it goes through the False side of the condition, meaning we can grab the error message from there.
True branch
Let’s focus on the True side of the condition. As we mentioned above, the details of the error information will not be present directly in the run history. Instead, we will have something like this:
"status": "Failed",
"code": "NotSpecified"
},
"id": "/subscriptions/XXXXXX/resourceGroups/RG-DEMO-LASTD-MAIN/providers/Microsoft.Logic/workflows/LA-catchError-POC/runs/08585259124251183879640913293CU37/actions/Condition_3",
"name": "Condition_3",
"type": "Microsoft.Logic/workflows/runs/actions"
},
{
"properties": {
"outputsLink": {
"uri": "https://XXXX-XX.westeurope.logic.azure.com:XXX/workflows/XXXXXX/runs/08585259124251183879640913293CU37/actions/Condition_4/contents/ActionOutputs?api-version=2016-06-01&se=2023-02-06T20%3A00%3A00.0000000Z&sp=%2Fruns%2F08585259124251183879640913293CU37%2Factions%2FCondition_4%2Fcontents%2FActionOutputs%2Fread&sv=1.0&sig=XXXXX",
"contentVersion": "XXXXXX==",
"contentSize": 709,
"contentHash": {
"algorithm": "md5",
"value": "XXXXX=="
The status is Failed, but there are no error message details to present in this situation, so the error must be somewhere else, and indeed it is. The error, in this case, is present as a URL, in the object uri, so if we follow this link and paste it into a browser, this is what we receive in our sample:
{"error":{"code":"AuthorizationFailed","message":"The authentication credentials are not valid."}}
That means for us to get the correct error message, we have to get that link and perform another HTTP call, and this is exactly what we are going to do with the next action:
- So, on the true branch of the condition, add a new HTTP action with the following configuration:
- Set the Method property as GET.
- Set the URI property to be dynamic using the output of the filter array:
- items(‘For_each_Filter_Array_Run_History’)?[‘properties’]?[‘outputsLink’]?[‘uri’]
But unfortunately, even the response that comes from this URI with the error detail can appear in different ways. We have already found two scenarios:
- The error can appear in this time of structure:
{
"statusCode": 404,
"headers": {
"Pragma": "no-cache",
"x-ms-failure-cause": "gateway",
"x-ms-request-id": "XXXXXXX",
"x-ms-correlation-request-id": "XXXXXX",
"x-ms-routing-request-id": "XXXXXX",
"Strict-Transport-Security": "max-age=31536000; includeSubDomains",
"X-Content-Type-Options": "nosniff",
"Cache-Control": "no-cache",
"Date": "Fri, 03 Feb 2023 12:19:12 GMT",
"Content-Length": "302",
"Content-Type": "application/json; charset=utf-8",
"Expires": "-1"
},
"body": {
"error": {
"code": "InvalidResourceType",
"message": "The resource type 'workflows' could not be found in the namespace 'Microsoft.Logic' for api version '2016-06-01''. The supported api-versions are '2015-02-01-preview,2015-08-01-preview,2016-06-01,2016-10-01,2017-07-01,2018-07-01-preview,2019-05-01'."
}
}
}
- And sometimes like this:
[
{
"name": "Condition",
"startTime": "2023-02-06T10:21:38.4195084Z",
"endTime": "2023-02-06T10:21:38.4195084Z",
"trackingId": "fd8b62ec-4745-4e85-84b4-da57b8e8b8c2",
"clientTrackingId": "08585259279877955762218280603CU192",
"code": "BadRequest",
"status": "Failed",
"error": {
"code": "InvalidTemplate",
"message": "Unable to process template language expressions for action 'Condition' at line '0' and column '0': 'The template language function 'startsWith' expects its first parameter to be of type string. The provided value is of type 'Null'. Please see https://aka.ms/logicexpressions#startswith for usage details.'."
}
}
]
For that reason, and in order to try to provide the best detail possible, we decide to create a variable to set up the type of error we are dealing with:
- First, we add a Parse JSON action to parse the response of the previous HTTP Call using the following schema:
{
"items": {
"properties": {
"body": {
"properties": {
"error": {
"properties": {
"code": {
"type": "string"
},
"message": {
"type": "string"
}
},
"type": "object"
}
},
"type": "object"
},
"clientTrackingId": {
"type": "string"
},
"code": {
"type": "string"
},
"endTime": {
"type": "string"
},
"error": {
"properties": {
"code": {
"type": "string"
},
"message": {
"type": "string"
}
},
"type": "object"
},
"name": {
"type": "string"
},
"startTime": {
"type": "string"
},
"status": {
"type": "string"
},
"trackingId": {
"type": "string"
}
},
"required": [
"name",
"startTime",
"endTime",
"trackingId",
"clientTrackingId",
"status"
],
"type": "object"
},
"type": "array"
}
- And then a Variables – Set variable action with the following condition to define the type of the error;
- if(equals(first(body(‘Parse_Outputs_URL’))?[‘error’],null),if(equals(first(body(‘Parse_Outputs_URL’))?[‘body’]?[‘error’],null),’Default’,’BodyError’),’Error’)
What this expression does?
- It first checks if the error key’s value in the response body’s first element equals null.
- If that evaluates to true, it then checks if the value of the error key in the body object of the first element of the response body is equal to null.
- If this second check is true, it returns the string Default – this deals with unpredict structures.
- If the second check is false, it returns the string BodyError.
- If the first check is false, it returns the string Error.
- If that evaluates to true, it then checks if the value of the error key in the body object of the first element of the response body is equal to null.
To define the response with the correct error message structure, we will be adding a Switch action with 3 branches:
- Case – Error
- Inside this branch, we will be defining the value of another variable – that will be our response structure – to be:
{
"name": "@{items('For_each_Filter_Array_Run_History')?['name']}",
"type": "@{items('For_each_Filter_Array_Run_History')?['type']}",
"status": "@{items('For_each_Filter_Array_Run_History')?['properties']?['status']}",
"code": "@{items('For_each_Filter_Array_Run_History')?['properties']?['code']}",
"startTime": "@{items('For_each_Filter_Array_Run_History')?['properties']?['startTime']}",
"endTime": "@{items('For_each_Filter_Array_Run_History')?['properties']?['endTime']}",
"errorMessage": "@{first(body('Parse_Outputs_URL'))?['error']?['message']}"
}
- Case – Body Error
- Inside this branch, we will be defining the value of another variable – that will be our response structure – to be:
{
"name": "@{items('For_each_Filter_Array_Run_History')?['name']}",
"type": "@{items('For_each_Filter_Array_Run_History')?['type']}",
"status": "@{items('For_each_Filter_Array_Run_History')?['properties']?['status']}",
"code": "@{items('For_each_Filter_Array_Run_History')?['properties']?['code']}",
"startTime": "@{items('For_each_Filter_Array_Run_History')?['properties']?['startTime']}",
"endTime": "@{items('For_each_Filter_Array_Run_History')?['properties']?['endTime']}",
"errorMessage": "@{first(body('Parse_Outputs_URL'))?['body']['error']?['message']}"
}
- and Default
- And finally, inside this branch, we will be defining the value of another variable – that will be our response structure – to be:
{
"name": "@{items('For_each_Filter_Array_Run_History')?['name']}",
"type": "@{items('For_each_Filter_Array_Run_History')?['type']}",
"status": "@{items('For_each_Filter_Array_Run_History')?['properties']?['status']}",
"code": "@{items('For_each_Filter_Array_Run_History')?['properties']?['code']}",
"startTime": "@{items('For_each_Filter_Array_Run_History')?['properties']?['startTime']}",
"endTime": "@{items('For_each_Filter_Array_Run_History')?['properties']?['endTime']}",
"errorMessage": @{body('Call_Outputs_URL')}
}
False branch
Now, let’s focus on the False side of the condition. As we mentioned previously, in this scenario, we must carefully discard the following generic messages that can appear: An action failed. No dependent actions succeeded.
To do that, we will be adding a Condition with the following configuration:
- items(‘For_each_Filter_Array_Run_History’)?[‘properties’]?[‘error’]?[‘message’] is not equal to An action failed. No dependent actions succeeded.
If the error is the generic one, it will ignore it, and it will go to the next error in the error history. Note: if we have this generic error message, we will always have two Failed actions – we will always want the other detail because it is there that we will have the real error detailed message.
If the error is not the generic one, then we are going to define the output message in our support variable using the following expression:
{
"name": "@{items('For_each_Filter_Array_Run_History')?['name']}",
"type": "@{items('For_each_Filter_Array_Run_History')?['type']}",
"status": "@{items('For_each_Filter_Array_Run_History')?['properties']?['status']}",
"code": "@{items('For_each_Filter_Array_Run_History')?['properties']?['code']}",
"startTime": "@{items('For_each_Filter_Array_Run_History')?['properties']?['startTime']}",
"endTime": "@{items('For_each_Filter_Array_Run_History')?['properties']?['endTime']}",
"errorMessage": "@{items('For_each_Filter_Array_Run_History')?['properties']?['error']?['message']}"
}
as you can see in this picture:
To finalize, we just need to add the Response action with the following configuration:
- Set the Status Code as 200
- Add the following header in the Headers properties:
- Content-Type: application/json
- Set the Body property with that value of our output message variable that we used previously
In the end, our Logic App will look like this:
To finalize and for this to work properly, we need to configure the Logic App managed identity and permissions.
Configure the Logic App managed identity and permissions
We already described in the beginning that we need to enable the managed identity, but in order for this Logic App to be able to extract the error information from other Logic App run histories, we need to give to that managed identity the Logic App Operator role in each resource group that contains the Logic App from which we want to access and read the run history.
For this last step:
- On the Azure Portal, access the Resource Group that has our Logic App from where we want to grab the correct error message.
- On the Resource Group page, select the option Access control (IAM).
- On the Access control (IAM) panel, click Add > Add role assignment
- On the Add role assignment page, on the Role tab, search for Logic App and then select the option Logic App Operator, and then click Next.
- On the Members tab, select the Assign access to be Managed identity, and from the Members:
- Select your subscription on the subscription property.
- On the Managed identity list of options, select our Logic App Catch Error
- and on the Select property, select the managed identity of our function and then click Close.
- Click on Review + Assign and then Review + Assign again.
We can now use this Generic Logic App to read the error detail inside our other Logic Apps.
Where can I download it
You can download the complete Azure Function source code here:
Credits
Kudu to my team member Luis Rigueira for participating in this proof-of-concept!
Hope you find this useful! So, if you liked the content or found it useful and want to help me write more content, you can buy (or help buy) my son a Star Wars Lego!