One of the many great features of Logic Apps is its support for long running asynchronous workflows through the ‘202 async’ pattern. Although not standardised in any official specification as far as I know, the ‘202 async’ pattern is commonly used to interact with APIs in an asynchronous way through polling. In summary, this pattern informs an API consumer that an API call is accepted (HTTP status 202) accompanied by a callback URL where the API consumer can regularly check for an actual response payload. The callback URL is provided in the HTTP location response header. In addition Logic Apps provides a recommendation regarding the polling interval through a ‘Retry-After’ HTTP response header. To demonstrate how this magic works in action I’ve created a small Logic App with a built-in delay of one minute. Here’s the basic message flow:

If we call this Logic App from an HTTP client you’ll notice a response message after a minute. Not ideal, and scenarios like this will likely cause in timeouts if the operation even takes longer. Luckily Logic Apps makes it very easy to resolve this challenge with a simple flick of a switch on the settings of the HTTP response connector:

When we call the Logic App API with an HTTP client like Postman we receive a completely different response. Instead of the expected response payload we get a bunch of Logic App run properties. The status property indicates that the workflow is still running.

{
    "properties": {
        "waitEndTime": "2018-12-12T00:50:09.7523799Z",
        "startTime": "2018-12-12T00:50:09.7523799Z",
        "status": "Running",
        "correlation": {
            "clientTrackingId": "08586570310757255241595533212CU31"
        },
        "workflow": {
            "id": "/workflows/04c74a7043984ffd8bda2dc2437a6bf7/versions/08586586828895236601",
            "name": "08586586828895236601",
            "type": "Microsoft.Logic/workflows/versions"
        },
        "trigger": {
            "name": "manual",
            "inputsLink": {
                "uri": "https://prod-18.australiaeast.logic.azure.com:443/workflows/04c74a7043984ffd8bda2dc2437a6bf7/runs/08586570310757255241595533212CU31/contents/TriggerInputs?api-version=2016-10-01&se=2018-12-12T04%3A00%3A00.0000000Z&sp=%2Fruns%2F08586570310757255241595533212CU31%2Fcontents%2FTriggerInputs%2Fread&sv=1.0&sig=xD7xM2pvbs-_RgX1FVcsq2MImu5rlt_WCfLq4tE9Qqc",
                "contentVersion": "HnyZbRBXXZ5RxDoTJydztQ==",
                "contentSize": 28,
                "contentHash": {
                    "algorithm": "md5",
                    "value": "HnyZbRBXXZ5RxDoTJydztQ=="
                }
            },
            "outputsLink": {
                "uri": "https://prod-18.australiaeast.logic.azure.com:443/workflows/04c74a7043984ffd8bda2dc2437a6bf7/runs/08586570310757255241595533212CU31/contents/TriggerOutputs?api-version=2016-10-01&se=2018-12-12T04%3A00%3A00.0000000Z&sp=%2Fruns%2F08586570310757255241595533212CU31%2Fcontents%2FTriggerOutputs%2Fread&sv=1.0&sig=TpIrjRJqorJLs621AEWFrDNzHcRfYKORA8YuVXq6e9A",
                "contentVersion": "DdHqLk/lmUN0iUdPwWVQ8A==",
                "contentSize": 277,
                "contentHash": {
                    "algorithm": "md5",
                    "value": "DdHqLk/lmUN0iUdPwWVQ8A=="
                }
            },
            "startTime": "2018-12-12T00:50:09.7506184Z",
            "endTime": "2018-12-12T00:50:09.7506184Z",
            "originHistoryName": "08586570310757255241595533212CU31",
            "correlation": {
                "clientTrackingId": "08586570310757255241595533212CU31"
            },
            "status": "Succeeded"
        },
        "outputs": {},
        "response": {
            "startTime": "2018-12-12T00:50:09.7506184Z",
            "correlation": {},
            "status": "Waiting"
        }
    },
    "id": "/workflows/04c74a7043984ffd8bda2dc2437a6bf7/runs/08586570310757255241595533212CU31",
    "name": "08586570310757255241595533212CU31",
    "type": "Microsoft.Logic/workflows/runs"
}

If we look at the response HTTP headers we notice a status of ‘202 Accepted’, a location header and a suggested polling retry interval.

If we immediately follow the link from the location header we’ll get the same response message, until after about a minute. When the Logic App workflow has completed its run we’ll finally receive our Logic App response payload:

Pretty cool. Although I’m no huge fan of polling given the fact it’s so chatty and doesn’t result in a timely response, I think there’s certainly valid use cases for this scenario. API consumers could provide a callback channel in the request payload or header, but this has a drawback of every API consumer now all of the sudden becoming a provider too. Not feasible in most cases.

Azure API Management

As a general rule of thumb I don’t expose Logic App APIs directly to consumers and mediate them through either an Azure function proxy or Azure API Management. Azure API Management has built-in integration with Logic Apps, and especially with the recent addition of the consumption tier in Azure API Management it’s a great way of abstracting your API implementations. Let’s create a basic API by adding our Logic App to our Azure API Management instance:

I’ve simply selected my Logic App ‘longrunning’ and associated it with my API product ‘Anonymous’, which doesn’t require a subscription and makes testing our API even easier. Next we’ll call our API through the Azure API Management test console.

Our API is successfully called through Azure API Management, hiding the Logic Apps trigger URL and exposed via the more static URL https://kloud.azure-api.net/longrunning/manual/paths/invoke

Very neat, but if we have a closer look at the API response we can notice the location header and trigger Uris in the response payload exposing our Logic App endpoint. Call it paranoia or API OCD, but wouldn’t it be better to have the subsequent polling API calls mediated through Azure API Management as well? This allows us to enforce policies like throttling, provides us the ability to identify our caller and captures traffic in Application Insights together with all other API calls. Bonus.

Run API, run!

Let’s have a closer look a the URL in the location header: 

https://prod-18.australiaeast.logic.azure.com/workflows/04c74a7043984ffd8bda2dc2437a6bf7/runs/08586570310757255241595533212CU31/operations/1d19015e-5f32-4d72-a92c-04a5da36084d?api-version=2016-10-01&sp=%2Fruns%2F08586570310757255241595533212CU31%2Foperations%2F1d19015e-5f32-4d72-a92c-04a5da36084d%2Fread&sv=1.0&sig=oU1qLxgdECjumvTai2mXMDn-0gKl0d3xJAM785JBMcI

And the trigger input/output URLs:

https://prod-18.australiaeast.logic.azure.com:443/workflows/04c74a7043984ffd8bda2dc2437a6bf7/runs/08586570310757255241595533212CU31/contents/TriggerInputs?api-version=2016-10-01&se=2018-12-12T04%3A00%3A00.0000000Z&sp=%2Fruns%2F08586570310757255241595533212CU31%2Fcontents%2FTriggerInputs%2Fread&sv=1.0&sig=xD7xM2pvbs-_RgX1FVcsq2MImu5rlt_WCfLq4tE9Qqc https://prod-18.australiaeast.logic.azure.com:443/workflows/04c74a7043984ffd8bda2dc2437a6bf7/runs/08586570310757255241595533212CU31/contents/TriggerOutputs?api-version=2016-10-01&se=2018-12-12T04%3A00%3A00.0000000Z&sp=%2Fruns%2F08586570310757255241595533212CU31%2Fcontents%2FTriggerOutputs%2Fread&sv=1.0&sig=TpIrjRJqorJLs621AEWFrDNzHcRfYKORA8YuVXq6e9A

The URLs all have a similar pattern of /workflows/<WorkflowId>/runs/<RunId>. The workflow ID is static and corresponds to our Logic App, the run ID belongs to every instance of a running Logic App. So what can we do to:

  • Rewrite URLs in our response headers and payload
  • Route calls through our Azure API Management instance to the correct Logic App instance

That’s where Azure API Management policies come to the rescue. First we’ll define an outbound processing policy on the ‘All operations’ level:

<policies>
    <inbound>
        <base />
        <set-backend-service id="apim-generated-policy" backend-id="LogicApp_longrunning_rg-logicdemo" />
    </inbound>
    <backend>
        <base />
    </backend>
    <outbound>
        <base />
        <find-and-replace from="prod-18.australiaeast.logic.azure.com/workflows/04c74a7043984ffd8bda2dc2437a6bf7" to="kloud.azure-api.net/longrunning" />
        <find-and-replace from="prod-18.australiaeast.logic.azure.com:443/workflows/04c74a7043984ffd8bda2dc2437a6bf7" to="kloud.azure-api.net/longrunning" />
        <set-header name="Location" exists-action="override">
            <value>@(context.Response.Headers.GetValueOrDefault("Location")?.Replace("prod-18.australiaeast.logic.azure.com/workflows/04c74a7043984ffd8bda2dc2437a6bf7","kloud.azure-api.net/longrunning"))</value>
        </set-header>
    </outbound>
    <on-error>
        <base />
    </on-error>
</policies>

When we call the API again we’ll notice both location header as well as all URLs in the response payload are now redirected to Azure API Management.

The next step is to map any calls to /longrunning/runs to a corresponding backend URL. Let’s define a new operation ‘runs’:

We’ll also need to override the backend URL and remove the <base /> policy which points to the Logic App trigger instead of run endpoint:

<policies>
    <inbound>
        <set-backend-service base-url="https://prod-18.australiaeast.logic.azure.com/workflows/04c74a7043984ffd8bda2dc2437a6bf7" />
    </inbound>
    <backend>
        <base />
    </backend>
    <outbound>
        <base />
    </outbound>
    <on-error>
        <base />
    </on-error>
</policies>

Last but not least, let’s test if the added operation actually works by performing a HTTP GET on the URL as specified in the location header. The first call results in a ‘202 accepted’:

Let’s try again after waiting for another minute:

The HTTP GET requests to our added API operations get successfully routed to our backend Logic App instance, and we’re receiving the expected payload after some asynchronous polling.

To summarise the required steps in order to abstract our Logic Apps backend URLs:

  • Configure your Logic App to be asynchronous in the HTTP response connector
  • Perform URL rewriting on the API level with a find-and-replace and location header override
  • Add a /runs operation to map the rewritten URLs to the corresponding Logic Apps backend URLs 
Category:
Application Development and Integration, Azure Platform, Uncategorized