Azure API Management cache policy

Some notes on writing an Azure APIM cache policy for an external Redis cache. There is also official documentation for this and you can look up each policy statement in the reference.

This page is extra wide to accomodate the ugly XML.1

Reading from the request

You can get path parameter and header values from the request as follows.

<set-variable 
    name="id" 
    value="@(context.Request.MatchedParameters["id"])" />
<set-variable 
    name="cachecontrol" 
    value="@((string)context.Request.Headers.GetValueOrDefault("Cache-Control", ""))" />

I’ll use the id value as part of the cache key and the Cache-Control header to allow the caller to disable caching. Both will be available as context.Variables from this point on.

Cache lookup

If no-cache is not set, I try to retrieve a cached value for the request’s id parameter and write it into another variable cachevalue.

<choose>
    <when condition="@(!((string)context.Variables["cachecontrol"]).Contains("no-cache"))">
        <cache-lookup-value 
            key="@("id-" + context.Variables["id"])" 
            variable-name="cachevalue" />
    </when>
</choose>

The following steps are only executed if cachevalue is not present. This works with <choose> and <when> again, but it’s ugly enough without these additional levels of nesting, so just imagine them.

Sending the actual request

<send-request mode="copy" response-variable-name="apiresponse" timeout="10" ignore-error="true">
    <set-header name="x-functions-key" exists-action="skip">
        <value>{{functionkey}}</value>
    </set-header>
</send-request>

The request is sent to the backend and the response is stored in a variable apiresponse. The backend in this case is an Azure Function and we have to set the x-functions-key as header for authorization – here a named value is used.

After that, we store the body of the response in cachevalue.

<set-variable 
    name="cachevalue" 
    value="@(((IResponse)context.Variables["apiresponse"]).Body.As<string>())" />

Cache write

If the user didn’t request no-store, we write what’s in cachevalue into the cache for later requests.

<choose>
    <when condition="@(!((string)context.Variables["cachecontrol"]).Contains("no-store"))">
    <cache-store-value 
        key="@("id-" + context.Variables["id"])" 
        value="@((string)context.Variables["cachevalue"])" 
        duration="9000000" />
    </when>
</choose>

You might think that what you specify as key will be used as the key in your Redis cache and certainly the documentation seems to support this idea:

key: Cache key the value will be stored under.

But Azure actually adds a prefix to the key (in my case “2_”) and doesn’t guarantee that it will always be the same prefix, so after an update it might change without warning. If you want to prefill or invalidate entries in your Redis cache, you need to set up another endpoint that manipulates the cache via Azure’s policy statements (which will then add the same prefix, whatever it may be).

Another somewhat surprising limitation is that you have to specify a duration, even if Redis supports permanent entries. So if you want to manage cache invalidation based on events, you can only specify a very long duration.

Response body

So far everything we did was in the inbound section of the policy. Now we need one final statement in the outbound section that sets the content of cachevalue as the body of our response.

<set-body>@((string)context.Variables["cachevalue"])</set-body>

Done.


  1. I think Azure did a decent job at preventing any useful syntax highlighting with their mix of XML and C# code, but if you want to be absolutely sure, just dump the whole policy as a string into your terraform config and you’re golden. /s↩︎