Azure API Management Policy Engine

Modifying Request/Response Behavior Without Code Changes


Introduction

The Azure API Management (APIM) policy engine is a powerful feature that allows you to modify how API requests and responses are handled without changing any backend code. Policies are XML-based statements that execute sequentially on incoming requests and outgoing responses.

This comprehensive guide covers:

  • Policy structure — Understanding the four policy sections
  • Common policies — Frequently used policy statements
  • Policy expressions — Using C# expressions in policies
  • Advanced patterns — Combining policies for complex scenarios
  • Best practices — Writing maintainable policies

Policy Execution Flow

Four Policy Sections

┌─────────────────────────────────────────────────────────────────────┐
│                    POLICY EXECUTION FLOW                            │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│   ┌──────────────────────────────────────────────────────────────┐  │
│   │                     INBOUND                                  │  │
│   │   • Transform request                                        │  │
│   │   • Validate authentication                                  │  │
│   │   • Rate limiting                                            │  │
│   │   • Set headers/variables                                    │  │
│   └────────────────────────────┬─────────────────────────────────┘  │
│                                │                                    │
│                                ▼                                    │
│   ┌──────────────────────────────────────────────────────────────┐  │
│   │                      BACKEND                                 │  │
│   │   • Modify request to backend                                │  │
│   │   • Route to different backend                               │  │
│   │   • Mock response                                            │  │
│   └────────────────────────────┬─────────────────────────────────┘  │
│                                │                                    │
│                                ▼                                    │
│   ┌──────────────────────────────────────────────────────────────┐  │
│   │                     OUTBOUND                                 │  │
│   │   • Transform response                                       │  │
│   │   • Add CORS headers                                         │  │
│   │   • Cache response                                           │  │
│   │   • Set headers                                              │  │
│   └────────────────────────────┬─────────────────────────────────┘  │
│                                │                                    │
│                                ▼                                    │
│   ┌──────────────────────────────────────────────────────────────┐  │
│   │                     ON-ERROR                                 │  │
│   │   • Handle errors                                            │  │
│   │   • Log errors                                               │  │
│   │   • Return custom error response                             │  │
│   └──────────────────────────────────────────────────────────────┘  │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Policy Structure

Basic Policy Template

<policies>
    <inbound>
        <!-- Policies executed before request is forwarded to backend -->
        <base />
    </inbound>
    
    <backend>
        <!-- Policies executed when forwarding request to backend -->
        <base />
    </backend>
    
    <outbound>
        <!-- Policies executed after response is received from backend -->
        <base />
    </outbound>
    
    <on-error>
        <!-- Policies executed when an error occurs -->
        <base />
    </on-error>
</policies>

Scope Levels

<!-- Global (All APIs) -->
<policies>
    <inbound>...</inbound>
</policies>

<!-- API Scope -->
<api name="orders-api">
    <inbound>...</inbound>
</api>

<!-- Operation Scope -->
<operation name="GetOrder">
    <inbound>...</inbound>
</operation>

<!-- Product Scope -->
<product name="premium">
    <inbound>...</inbound>
</product>

Common Inbound Policies

Set Request Header

<inbound>
    <!-- Add or modify a header -->
    <set-header name="X-Correlation-Id" exists-action="override">
        <value>@(context.Request.Id)</value>
    </set-header>
    
    <!-- Add header if not exists -->
    <set-header name="X-Request-Source" exists-action="skip">
        <value>APIM</value>
    </set-header>
    
    <!-- Remove header -->
    <set-header name="X-Remove-Me" exists-action="delete" />
</inbound>

Set Query Parameter

<inbound>
    <!-- Add default parameter -->
    <set-query-parameter name="api-version" exists-action="override">
        <value>2023-01-01</value>
    </set-query-parameter>
    
    <!-- Optional parameter -->
    <set-query-parameter name="page" exists-action="optional">
        <value>1</value>
    </set-query-parameter>
</inbound>

Set Variable

<inbound>
    <!-- Store value in variable -->
    <set-variable name="correlationId" value="@(Guid.NewGuid().ToString())" />
    
    <!-- Extract from header -->
    <set-variable name="userId" value="@(context.Request.Headers.GetValueOrDefault("X-User-Id", ""))" />
    
    <!-- Parse JWT -->
    <set-variable name="jwtClaims" value="@(context.Request.Headers.GetValueOrDefault("Authorization", "").Split(' ')[1])" />
</inbound>

Conditional Processing (Choose)

<inbound>
    <choose>
        <when condition="@(context.Request.Method == HttpMethod.Get)">
            <set-header name="X-Request-Type" exists-action="override">
                <value>Read</value>
            </set-header>
        </when>
        <when condition="@(context.Request.Method == HttpMethod.Post)">
            <set-header name="X-Request-Type" exists-action="override">
                <value>Create</value>
            </set-header>
        </when>
        <otherwise>
            <set-header name="X-Request-Type" exists-action="override">
                <value>Other</value>
            </set-header>
        </otherwise>
    </choose>
</inbound>

Common Backend Policies

Route to Different Backend

<backend>
    <!-- Route based on API version -->
    <choose>
        <when condition="@(context.Request.Url.Query.GetValueOrDefault("version") == "v2")">
            <set-backend-service base-url="https://api-v2.example.com" />
        </when>
        <otherwise>
            <set-backend-service base-url="https://api-v1.example.com" />
        </otherwise>
    </choose>
</backend>

Mock Response (for testing)

<backend>
    <!-- Return mock response without calling backend -->
    <mock-response status-code="200" content-type="application/json">
        <set-body>{
            "status": "success",
            "data": {
                "message": "This is a mock response"
            }
        }</set-body>
    </mock-response>
</backend>

Common Outbound Policies

Transform JSON Response

<outbound>
    <!-- Rename properties in response -->
    <json-transformer>
        <rename-json-element name="oldName" new-name="newName" />
    </json-transformer>
</outbound>

Add CORS Headers

<outbound>
    <set-header name="Access-Control-Allow-Origin" exists-action="override">
        <value>*</value>
    </set-header>
    <set-header name="Access-Control-Allow-Methods" exists-action="override">
        <value>GET, POST, PUT, DELETE, OPTIONS</value>
    </set-header>
    <set-header name="Access-Control-Allow-Headers" exists-action="override">
        <value>Content-Type, Authorization</value>
    </set-header>
</outbound>

Set Response Header

<outbound>
    <set-header name="X-Response-Time" exists-action="override">
        <value>@(DateTime.UtcNow.ToString("o"))</value>
    </set-header>
</outbound>

Cache Response

<outbound>
    <!-- Check cache first -->
    <cache-lookup vary-by-developer="true">
        <vary-by-header>Accept</vary-by-header>
        <vary-by-header>Accept-Encoding</vary-by-header>
    </cache-lookup>
    
    <!-- If not cached, cache the response -->
    <cache-store duration="3600" />
</outbound>

On-Error Policies

Custom Error Response

<on-error>
    <!-- Log the error -->
    <log verbosity="error">
        @(context.LastError.Message)
    </log>
    
    <!-- Return custom error -->
    <return-response>
        <set-status code="500" reason="Internal Server Error" />
        <set-header name="Content-Type" exists-action="override">
            <value>application/json</value>
        </set-header>
        <set-body>{
            "error": "An unexpected error occurred",
            "requestId": "@(context.Request.Id)"
        }</set-body>
    </return-response>
</on-error>

Retry and Fallback

<on-error>
    <choose>
        <when condition="@(context.Response.StatusCode == 503)">
            <!-- Try fallback backend -->
            <set-backend-service base-url="https://fallback-api.example.com" />
            <retry count="3" interval="5">
                <forward-request />
            </retry>
        </when>
        <otherwise>
            <return-response>
                <set-status code="500" />
                <set-body>Service temporarily unavailable</set-body>
            </return-response>
        </otherwise>
    </choose>
</on-error>

Policy Expressions

Access Context Variables

<!-- Request URL -->
<set-variable name="path" value="@(context.Request.Url.Path)" />

<!-- Request Method -->
<set-variable name="method" value="@(context.Request.Method.ToString())" />

<!-- Headers -->
<set-variable name="authToken" value="@(context.Request.Headers.GetValueOrDefault("Authorization", ""))" />

<!-- Response Status Code -->
<set-variable name="statusCode" value="@(context.Response.StatusCode)" />

Complex Expressions

<!-- Extract user ID from JWT -->
<set-variable name="userId" value="@{
    var authHeader = context.Request.Headers.GetValueOrDefault("Authorization", "");
    if (string.IsNullOrEmpty(authHeader)) return null;
    
    var token = authHeader.Split(' ')[1];
    var parts = token.Split('.');
    if (parts.Length != 3) return null;
    
    var payload = System.Text.Encoding.UTF8.GetString(
        System.Convert.FromBase64String(parts[1] + "=="));
    
    var json = System.Text.Json.JsonDocument.Parse(payload);
    return json.RootElement.GetProperty("sub").GetString();
}" />

Condition Examples

<inbound>
    <!-- Check if user is admin -->
    <choose>
        <when condition="@(context.Variables.GetValueOrDefault<bool>("isAdmin"))">
            <!-- Admin can access all -->
        </when>
        <otherwise>
            <!-- Non-admin: check resource ownership -->
            <set-variable name="canAccess" value="@{
                var userId = context.Request.Headers.GetValueOrDefault("X-User-Id", "");
                var resourceOwnerId = context.Request.Url.Query.GetValueOrDefault("ownerId", "");
                return userId == resourceOwnerId;
            }" />
        </otherwise>
    </choose>
</inbound>

Advanced Patterns

Rate Limiting with Custom Key

<inbound>
    <set-variable name="rateKey" value="@{
        var userId = context.Request.Headers.GetValueOrDefault("X-User-Id", "");
        var apiName = context.Api.Name;
        return $"{apiName}:{userId}";
    }" />
    
    <rate-limit-by-key key="@(context.Variables["rateKey"])" calls="100" renewal-period="60" />
</inbound>

Validate Request Body

<inbound>
    <validate-jwt header-name="Authorization">
        <issuer-signing-keys>
            <key>base64encodedkey</key>
        </issuer-signing-keys>
        <audiences>
            <audience>api://my-api</audience>
        </audiences>
        <issuers>
            <issuer>https://sts.windows.net/tenant-id/</issuer>
        </issuers>
    </validate-jwt>
</inbound>

Transform Request to Backend

<inbound>
    <!-- Convert XML to JSON -->
    <xml-to-json policy="transform" apply="always" consider-accept-header="false" />
    
    <!-- Set backend URL with path -->
    <set-backend-service base-url="https://backend.example.com/api/v2" />
</inbound>

Best Practices

PracticeDescription
Use base policyInclude <base /> to inherit parent policies
Organize with commentsAdd XML comments for clarity
Test in test consoleUse APIM test console to validate policies
Version policiesTrack policy changes in source control
Use named policiesCreate reusable policy fragments
Log errorsAlways include error logging

Debugging

<!-- Trace policy execution -->
<trace>
    @(context.Variables["variableName"])
</trace>

<!-- Log to APIM logs -->
<log level="information">
    Processing request: @(context.Request.Id)
</log>

Related Topics


Azure Integration Hub - Intermediate Level