Snippet

API Management policy that keeps the trace and drops caller baggage

A complete front-gateway policy: CORS for W3C headers, untrusted baggage removed, per-caller rate limits, and privacy-safe hop markers.

xmlOct 6, 2026MIT

AI-generated, human-reviewed: the first draft was generated by an AI agent; I fact-checked, tested, and edited it. AI use policy

What it does#

Lets the browser send traceparent and tracestate through CORS, deletes caller-supplied baggage at the trust boundary, rate-limits each caller by IP address, forwards to your API, and writes short hop markers to Application Insights. The trace headers pass through untouched.

You need#

  • API Management on a classic tier that supports rate-limit-by-key (Developer, Basic, Standard, or Premium; not Consumption)
  • Two named values: spa-origin and agent-api-backend-url
  • The API’s Application Insights diagnostics enabled with the W3C correlation protocol and an Application Insights logger at verbosity “information”

Where it goes#

Paste it as the API’s policy (all operations) in API Management.

api-policy.xml
<!-- SPDX-License-Identifier: MIT -->
<!-- Copyright (c) 2026 Sarang Datye -->
<!--
  Front gateway policy for a browser app calling an agent API.

  Named values to create in API Management:
    {{spa-origin}}              the browser app's origin, for example https://localhost:5173
    {{agent-api-backend-url}}   the agent API's base URL

  This policy does not set or overwrite traceparent or tracestate. API Management's built-in
  W3C correlation, enabled in the API's Application Insights diagnostics, propagates them.

  The <trace> elements add privacy-safe hop markers such as [SPA-TO-APIM] to Application
  Insights. They use static messages and APIM operation metadata; never caller-controlled
  paths, headers, query strings, or bodies. They need the Application Insights logger with
  verbosity "information".
-->
<policies>
  <inbound>
    <base />
    <trace source="application_apim_1" severity="information">
      <message>@("[SPA-TO-APIM] Request received from SPA")</message>
      <metadata name="operation" value="@(context.Operation.Name)" />
    </trace>
    <cors allow-credentials="false">
      <allowed-origins>
        <origin>{{spa-origin}}</origin>
      </allowed-origins>
      <allowed-methods>
        <method>GET</method>
        <method>POST</method>
        <method>DELETE</method>
        <method>OPTIONS</method>
      </allowed-methods>
      <allowed-headers>
        <header>Content-Type</header>
        <header>traceparent</header>
        <header>tracestate</header>
      </allowed-headers>
    </cors>
    <!--
      W3C baggage from a caller is untrusted. AgentApi would copy it onto spans and relay it to
      the AI gateway, so it is dropped here at the trust boundary. traceparent/tracestate pass
      through untouched.
    -->
    <set-header name="baggage" exists-action="delete" />
    <!--
      Per-user rate limit, keyed on the caller's IP. It sits here (not in AgentApi) because
      AgentApi only ever sees APIM's IP, so a per-IP limit there is one bucket shared by all
      users. Placed after cors so browser preflights are answered without being counted.
    -->
    <choose>
      <when condition="@(context.Request.Url.Path.EndsWith(&quot;/api/feedback&quot;))">
        <rate-limit-by-key calls="60" renewal-period="60" counter-key="@(&quot;feedback:&quot; + context.Request.IpAddress)" />
      </when>
      <otherwise>
        <rate-limit-by-key calls="30" renewal-period="60" counter-key="@(&quot;chat:&quot; + context.Request.IpAddress)" />
      </otherwise>
    </choose>
    <set-backend-service base-url="{{agent-api-backend-url}}" />
    <trace source="application_apim_1" severity="information">
      <message>@("[APIM-TO-API] Forwarding request to AgentApi backend")</message>
      <metadata name="operation" value="@(context.Operation.Name)" />
    </trace>
  </inbound>
  <backend>
    <base />
  </backend>
  <outbound>
    <base />
    <trace source="application_apim_1" severity="information">
      <message>@("[API-TO-APIM] Response received from AgentApi")</message>
    </trace>
    <trace source="application_apim_1" severity="information">
      <message>@("[APIM-TO-SPA] Returning response to SPA caller")</message>
    </trace>
  </outbound>
  <on-error>
    <base />
  </on-error>
</policies>

Check that it works#

Send a non-browser request with curl that includes both traceparent and baggage headers. In Application Insights, the [SPA-TO-APIM] and [APIM-TO-API] markers share the request’s operation ID, and your API never receives the baggage header. Browser CORS still allows only Content-Type, traceparent, and tracestate; baggage is deliberately not an allowed browser request header.