API Management — Solving Tracking Issues with send-request: mode="new" vs mode="copy"
The Problem
You have an APIM policy that calls an external service using send-request — maybe to log an audit event, enrich a request with data from another API, or notify a webhook. Everything looks fine in development. Then in production you start seeing:
- The external call carries headers the client sent that you never intended to forward
- Authorization tokens from the original request leak into your internal service call
- Correlation IDs get duplicated or corrupted because the outbound request inherited them
- Your logging service rejects calls because it receives unexpected headers
- Debugging is a nightmare — the
send-requestcall behaves differently depending on what the original client sends
All of these are tracking and context-leakage issues — and they all trace back to one thing: not understanding the difference between mode="new" and mode="copy" in the send-request policy.
What Is the send-request Policy?
send-request lets an APIM policy make an outbound HTTP call to any URL — during request processing, before the backend is called, or during the response. It is the Swiss Army knife of APIM policies.
<send-request mode="new" response-variable-name="myResponse" timeout="10" ignore-error="false">
<set-url>https://my-internal-service.com/api/notify</set-url>
<set-method>POST</set-method>
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<set-body>{"event": "api-called"}</set-body>
</send-request>
What You Can Do With It
Client Request
│
▼
┌─────────────────────────────────────────────────────────────┐
│ APIM Inbound Policy │
│ │
│ ① Enrich ──► send-request to data service │
│ ◄── get user profile, inject into request │
│ │
│ ② Auth ──► send-request to token validation service │
│ ◄── validate token, allow or deny │
│ │
│ ③ Audit ──► send-request to logging/audit service │
│ (fire-and-forget, ignore-error="true") │
└─────────────────────────────────────────────────────────────┘
│
▼
Backend API
│
▼
┌─────────────────────────────────────────────────────────────┐
│ APIM Outbound Policy │
│ │
│ ④ Enrich ──► send-request to transform response data │
│ ⑤ Notify ──► send-request to webhook on success │
└─────────────────────────────────────────────────────────────┘
The response-variable-name
The result of send-request is stored in a context variable you name. You can then read it:
<send-request mode="new" response-variable-name="tokenValidationResult" timeout="5">
<set-url>https://auth.internal/validate</set-url>
<set-method>POST</set-method>
<set-body>@(context.Request.Headers.GetValueOrDefault("Authorization", ""))</set-body>
</send-request>
<!-- Read the result later in the same policy -->
<set-variable name="isValid" value="@{
var result = (IResponse)context.Variables["tokenValidationResult"];
return result.StatusCode == 200;
}" />
The Core Difference: mode="new" vs mode="copy"
This is the most important concept in send-request. Every tracking and context-leakage bug you will ever hit comes from using the wrong mode.
mode="copy" — Inherits Everything From the Original Request
When you use mode="copy", APIM starts the outbound request as a full copy of the current inbound request. This means:
- All original request headers are copied (Authorization, Request-Id, User-Agent, Cookie, X-Forwarded-For, etc.)
- The original request body is copied
- The original request URL is used as a starting point (you then override it with
<set-url>) - The original request method is copied (you then override it with
<set-method>if needed)
mode="copy" behaviour:
┌──────────────────────────────────────────────────────┐
│ Original Client Request │
│ Headers: │
│ Authorization: Bearer eyJhbGc... │
│ Request-Id: abc-123 │
│ X-Custom-Client-Header: sensitive-value │
│ Cookie: session=xyz │
│ Body: { "userId": 42 } │
└──────────────────┬───────────────────────────────────┘
│ mode="copy" clones ALL of this
▼
┌──────────────────────────────────────────────────────┐
│ send-request outbound call │
│ Headers: ← ALL copied, even ones you didn't want │
│ Authorization: Bearer eyJhbGc... ← LEAKED │
│ Request-Id: abc-123 ← DUPLICATED │
│ X-Custom-Client-Header: sensitive-value ← LEAKED │
│ Cookie: session=xyz ← LEAKED │
│ Content-Type: application/json ← your addition │
│ Body: { "userId": 42 } ← ORIGINAL body, not yours │
└──────────────────────────────────────────────────────┘
mode="new" — Starts With a Blank Slate
When you use mode="new", APIM creates a completely fresh, empty request. Nothing from the original request is inherited. You explicitly set every header, body, method, and URL you want.
mode="new" behaviour:
┌──────────────────────────────────────────────────────┐
│ Original Client Request │
│ Headers: Authorization, Request-Id, Cookie... │
│ Body: { "userId": 42 } │
└──────────────────┬───────────────────────────────────┘
│ mode="new" → completely ignored
▼
┌──────────────────────────────────────────────────────┐
│ send-request outbound call │
│ Headers: ← ONLY what you explicitly set │
│ Content-Type: application/json ← yours │
│ Request-Id: your-generated-guid ← yours │
│ X-Api-Key: {{InternalApiKey}} ← yours │
│ Body: { "event": "api-called" } ← yours │
│ Method: POST ← yours │
└──────────────────────────────────────────────────────┘
Why Tracking Breaks With mode="copy"
Here are the exact scenarios where mode="copy" causes tracking and context issues.
Problem 1: Correlation ID Duplication
Your observability setup injects a Request-Id into every request. With mode="copy", the external call already has Request-Id copied from the client. If your fragment or policy then tries to set it again:
<!-- mode="copy" — Request-Id already exists from the client request -->
<send-request mode="copy" response-variable-name="auditResult" timeout="5">
<set-url>https://audit.internal/log</set-url>
<set-method>POST</set-method>
<!-- You try to set it, but exists-action="skip" means the COPIED one wins -->
<set-header name="Request-Id" exists-action="skip">
<value>@(context.Variables.GetValueOrDefault<string>("requestId"))</value>
</set-header>
</send-request>
Result: The audit service receives the client's original correlation ID, not the one your gateway generated. Your trace is now broken — the audit log and the gateway log have different IDs for the same transaction.
Even worse with exists-action="override": You override it, but now the copied body is still the original client body — not your audit payload.
Problem 2: Authorization Token Leaking to Internal Services
<!-- Dangerous: mode="copy" sends the client's Bearer token to your internal audit service -->
<send-request mode="copy" response-variable-name="auditLog" timeout="3" ignore-error="true">
<set-url>https://audit.internal/events</set-url>
<set-method>POST</set-method>
<set-body>{"event": "order-placed", "orderId": "123"}</set-body>
<!-- You set the body correctly, but the Authorization header was already copied -->
<!-- Your audit service now receives the client's Bearer token it didn't ask for -->
</send-request>
Result: Your internal audit service sees an Authorization: Bearer <client-token> header. If your audit service validates tokens, it may reject the call because the client token wasn't issued for the audit service audience. Security teams will flag this as credential forwarding.
Problem 3: Unexpected Headers Break Downstream Services
Client sends non-standard headers: X-Requested-With, X-Debug-Mode: true, Accept-Language, Referer. With mode="copy" all of these land on your internal service. Some internal services have strict header validation and reject unknown headers with 400 Bad Request. You spend hours debugging why your send-request is intermittently failing — the answer is a random header the client sometimes sends.
Problem 4: Body Mismatch
With mode="copy", the body starts as the original client request body. If you then set a new body with <set-body>, it overwrites — but only if the Content-Type is compatible. In some edge cases (multipart requests, binary bodies), the copied body can interfere with your intended payload.
Problem 5: Third-Party Caller Sends Their Own Request-Id — You Lose It or Misuse It
This is a real-world scenario that catches teams off guard. A third-party partner calls your main API and includes their own request tracking header — typically Request-Id, X-Request-Id, or traceparent. This is their ID, generated on their side, used to correlate the call in their own logs.
Third-Party System Your APIM Gateway
│ │
│ POST /api/orders │
│ Request-Id: tp-abc-789 ─────────►│
│ Authorization: Bearer <token> │
│ Content-Type: application/json │
│ │
There are two problems that commonly occur here:
Problem A — You discard their Request-Id entirely:
Your gateway generates a new Request-Id and never records the third party's Request-Id. When the third party opens a support ticket saying "our request tp-abc-789 failed", you have no way to find it in your logs — their ID and your ID were never linked.
<!-- Bad: generates a new ID, third-party's Request-Id is silently thrown away -->
<set-variable name="requestId" value="@(Guid.NewGuid().ToString())" />
Problem B — With mode="copy", their Request-Id leaks into your internal send-request calls:
Your audit service, enrichment service, or token service now receives Request-Id: tp-abc-789 in its headers — a third-party ID it has no context for. Worse, if your internal services use Request-Id for their own tracing, the third-party value corrupts their trace chain.
<!-- Dangerous: mode="copy" forwards the third-party's Request-Id
to your internal audit service as-is -->
<send-request mode="copy" response-variable-name="auditResult" timeout="3" ignore-error="true">
<set-url>https://audit.internal/events</set-url>
<set-method>POST</set-method>
<!-- audit.internal now receives Request-Id: tp-abc-789 from the third party -->
<!-- Your internal trace is now polluted with an external ID -->
</send-request>
The correct approach — capture their Request-Id, link it to yours, keep them separate:
<!-- Step 1: Read the third-party's Request-Id from the incoming request -->
<set-variable name="thirdPartyRequestId" value="@{
var tpId = context.Request.Headers.GetValueOrDefault("Request-Id", "");
if (string.IsNullOrEmpty(tpId)) {
tpId = context.Request.Headers.GetValueOrDefault("X-Request-Id", "");
}
return tpId;
}" />
<!-- Step 2: Generate your own internal correlation ID regardless -->
<set-variable name="requestId" value="@(Guid.NewGuid().ToString())" />
<!-- Step 3: Expose your correlation ID in the response
so the third party can also reference it in support tickets -->
<set-header name="Request-Id" exists-action="override">
<value>@(context.Variables.GetValueOrDefault<string>("requestId"))</value>
</set-header>
<!-- Step 4: In your audit send-request (mode="new"), log BOTH IDs together -->
<send-request mode="new" response-variable-name="auditIgnored" timeout="2" ignore-error="true">
<set-url>https://audit.internal/events</set-url>
<set-method>POST</set-method>
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<set-header name="X-Api-Key" exists-action="override">
<value>{{AuditApiKey}}</value>
</set-header>
<!-- Your correlation ID on the wire — not the third party's -->
<set-header name="Request-Id" exists-action="override">
<value>@(context.Variables.GetValueOrDefault<string>("requestId"))</value>
</set-header>
<set-body>@{
return new JObject(
new JProperty("requestId", context.Variables.GetValueOrDefault<string>("requestId")),
// Link third-party ID in the payload — now both IDs are searchable in your logs
new JProperty("thirdPartyRequestId", context.Variables.GetValueOrDefault<string>("thirdPartyRequestId", "")),
new JProperty("api", context.Api.Name),
new JProperty("operation", context.Operation.Name),
new JProperty("subscriptionId", context.Subscription?.Id ?? "anonymous"),
new JProperty("timestamp", DateTime.UtcNow.ToString("o"))
).ToString();
}</set-body>
</send-request>
Result: Your audit log now contains both requestId: "your-guid" and thirdPartyRequestId: "tp-abc-789". When a third-party partner raises a support ticket with their ID, you can search either field and find the exact transaction. Their ID never leaks into your internal service headers — it is recorded as data in the audit payload only.
Audit Log Entry:
{
"requestId": "f3a1b2c4-...", ← your internal ID, on all headers
"thirdPartyRequestId": "tp-abc-789", ← their ID, in payload only, searchable
"api": "Orders API",
"operation": "CreateOrder",
"subscriptionId": "sub-partner-xyz",
"timestamp": "2026-01-12T10:30:00Z"
}
The Fix: Always Use mode="new" for Internal Calls
The rule is simple:
If you are making a call to an internal service, a webhook, a logging endpoint, or any service that is NOT supposed to receive the client's context — use
mode="new".
<!-- CORRECT: mode="new" — full control, zero leakage -->
<send-request mode="new" response-variable-name="auditResult" timeout="5" ignore-error="true">
<set-url>https://audit.internal/events</set-url>
<set-method>POST</set-method>
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<set-header name="X-Api-Key" exists-action="override">
<value>{{AuditServiceApiKey}}</value>
</set-header>
<set-header name="Request-Id" exists-action="override">
<value>@(context.Variables.GetValueOrDefault<string>("requestId"))</value>
</set-header>
<set-body>@{
return new JObject(
new JProperty("event", "api-called"),
new JProperty("api", context.Api.Name),
new JProperty("operation", context.Operation.Name),
new JProperty("subscriptionId", context.Subscription.Id),
new JProperty("timestamp", DateTime.UtcNow.ToString("o")),
new JProperty("requestId", context.Variables.GetValueOrDefault<string>("requestId"))
).ToString();
}</set-body>
</send-request>
When IS mode="copy" the Right Choice?
mode="copy" is useful in a narrow, specific scenario: when you are acting as a transparent proxy and want to forward the original request — headers and all — to a different backend, with minimal modifications.
Legitimate Use Case: Request Forwarding / Shadow Traffic
<!-- Shadow traffic: send a copy of the real request to a new service version for testing -->
<!-- You WANT all the original headers forwarded — that's the whole point -->
<send-request mode="copy" response-variable-name="shadowResponse" timeout="3" ignore-error="true">
<set-url>https://new-backend-v2.internal/api/orders</set-url>
<!-- No header overrides — you want exactly what the client sent -->
<!-- No body override — you want exactly what the client sent -->
</send-request>
<!-- Discard shadowResponse — only used for comparison testing, real response still comes from v1 -->
Legitimate Use Case: Protocol Bridge With Full Context Pass-Through
<!-- Forwarding to a secondary backend that needs the full client context:
same auth token, same correlation chain, same body -->
<send-request mode="copy" response-variable-name="secondaryResult" timeout="10">
<set-url>https://secondary-backend.internal/process</set-url>
<!-- Only override what's different — everything else passes through intentionally -->
<set-header name="X-Forwarded-Host" exists-action="override">
<value>@(context.Request.OriginalUrl.Host)</value>
</set-header>
</send-request>
Decision Table
| Scenario | Correct Mode | Reason |
|---|---|---|
| Audit / logging call | new | Internal service, no client context needed |
| Webhook notification | new | Only your payload, no client headers |
| Token validation call | new | You control exactly what you send |
| Data enrichment call | new | Clean request, only your params |
Third-party calls with their own Request-Id | new | Capture their ID as audit payload data; keep wire headers clean |
| Shadow traffic testing | copy | You want identical replica of client request |
| Transparent proxy forwarding | copy | Full client context pass-through is intentional |
| A/B routing to new backend | copy | Same request, different destination |
Benefits of send-request Policy
Beyond fixing the mode confusion, it helps to understand why send-request is so powerful and when to reach for it.
1. Pre-Request Enrichment
Fetch data before the backend sees the request. Inject it as a header or body field:
<!-- Fetch user profile and inject tier into the request -->
<send-request mode="new" response-variable-name="userProfile" timeout="5">
<set-url>@("https://users.internal/profile/" + context.Request.Headers.GetValueOrDefault("X-User-Id", ""))</set-url>
<set-method>GET</set-method>
<set-header name="X-Api-Key" exists-action="override">
<value>{{UserServiceApiKey}}</value>
</set-header>
</send-request>
<set-variable name="userTier" value="@{
var profile = (IResponse)context.Variables["userProfile"];
var body = profile.Body.As<JObject>();
return body["tier"]?.ToString() ?? "free";
}" />
<set-header name="X-User-Tier" exists-action="override">
<value>@(context.Variables.GetValueOrDefault<string>("userTier"))</value>
</set-header>
2. External Token Acquisition (On-Behalf-Of)
Fetch a service-to-service token before calling a protected backend:
<!-- Get a token for the backend service -->
<send-request mode="new" response-variable-name="backendToken" timeout="10">
<set-url>https://login.microsoftonline.com/{{TenantId}}/oauth2/v2.0/token</set-url>
<set-method>POST</set-method>
<set-header name="Content-Type" exists-action="override">
<value>application/x-www-form-urlencoded</value>
</set-header>
<set-body>@{
return "grant_type=client_credentials"
+ "&client_id={{ClientId}}"
+ "&client_secret={{ClientSecret}}"
+ "&scope={{BackendScope}}";
}</set-body>
</send-request>
<set-variable name="accessToken" value="@{
var tokenResponse = (IResponse)context.Variables["backendToken"];
var body = tokenResponse.Body.As<JObject>();
return body["access_token"]?.ToString() ?? "";
}" />
<set-header name="Authorization" exists-action="override">
<value>@("Bearer " + context.Variables.GetValueOrDefault<string>("accessToken"))</value>
</set-header>
3. Fire-and-Forget Audit Logging
Use ignore-error="true" so a logging failure never blocks the real request:
<!-- Non-blocking audit log — client never waits for this -->
<send-request mode="new" response-variable-name="auditIgnored" timeout="2" ignore-error="true">
<set-url>https://audit.internal/events</set-url>
<set-method>POST</set-method>
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<set-header name="X-Api-Key" exists-action="override">
<value>{{AuditApiKey}}</value>
</set-header>
<set-body>@{
return new JObject(
new JProperty("api", context.Api.Name),
new JProperty("operation", context.Operation.Name),
new JProperty("method", context.Request.Method),
new JProperty("url", context.Request.Url.ToString()),
new JProperty("subscriptionId", context.Subscription?.Id ?? "anonymous"),
new JProperty("ip", context.Request.IpAddress),
new JProperty("timestamp", DateTime.UtcNow.ToString("o"))
).ToString();
}</set-body>
</send-request>
<!-- auditIgnored is never read — we don't care about the response -->
4. Runtime Feature Flag Check
Call a feature flag service to decide policy behaviour:
<send-request mode="new" response-variable-name="featureFlags" timeout="3" ignore-error="true">
<set-url>@("https://flags.internal/check?feature=new-pricing&env=" + "{{EnvironmentName}}")</set-url>
<set-method>GET</set-method>
<set-header name="X-Api-Key" exists-action="override">
<value>{{FeatureFlagApiKey}}</value>
</set-header>
</send-request>
<set-variable name="useNewPricing" value="@{
try {
var resp = (IResponse)context.Variables["featureFlags"];
if (resp?.StatusCode == 200) {
return resp.Body.As<JObject>()["enabled"]?.Value<bool>() ?? false;
}
} catch {}
return false;
}" />
5. Webhook Notification on Response
Trigger a webhook after the backend responds successfully:
<!-- In outbound section — only fires on 2xx -->
<choose>
<when condition="@(context.Response.StatusCode >= 200 && context.Response.StatusCode < 300)">
<send-request mode="new" response-variable-name="webhookResult" timeout="3" ignore-error="true">
<set-url>{{WebhookUrl}}</set-url>
<set-method>POST</set-method>
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<set-header name="X-Webhook-Secret" exists-action="override">
<value>{{WebhookSecret}}</value>
</set-header>
<set-body>@{
return new JObject(
new JProperty("event", "request-succeeded"),
new JProperty("statusCode", context.Response.StatusCode),
new JProperty("operation", context.Operation.Name),
new JProperty("timestamp", DateTime.UtcNow.ToString("o"))
).ToString();
}</set-body>
</send-request>
</when>
</choose>
Solving the Tracking Issue End-to-End
Here is a complete, production-ready pattern that solves every tracking issue — correlation ID, context isolation, and clean audit logging — all in one policy.
Step 1: Inject Correlation ID First (Before Any send-request)
When a third-party caller sends their own Request-Id, you capture it separately and link it — never use it as your internal correlation ID, and never let it bleed onto your internal service headers.
<!-- Always do this FIRST in inbound, before any send-request calls -->
<!-- Capture third-party's Request-Id if present (check both common header names) -->
<set-variable name="thirdPartyRequestId" value="@{
var tpId = context.Request.Headers.GetValueOrDefault("Request-Id", "");
if (string.IsNullOrEmpty(tpId)) {
tpId = context.Request.Headers.GetValueOrDefault("X-Request-Id", "");
}
return tpId; // Empty string if not a third-party call — that's fine
}" />
<!-- Always generate YOUR OWN internal correlation ID -->
<!-- If the caller sent Request-Id (a trusted internal caller), honour it;
otherwise generate a fresh one — never blindly trust a third-party ID -->
<set-variable name="requestId" value="@{
var existing = context.Request.Headers.GetValueOrDefault("Request-Id", "");
return string.IsNullOrEmpty(existing) ? Guid.NewGuid().ToString() : existing;
}" />
<!-- Stamp your correlation ID on the request and response — not the third-party's -->
<set-header name="Request-Id" exists-action="override">
<value>@(context.Variables.GetValueOrDefault<string>("requestId"))</value>
</set-header>
Step 2: Use mode="new" With Explicit Correlation in Every send-request
<!-- Every internal send-request carries YOUR correlation ID, not the client's -->
<send-request mode="new" response-variable-name="enrichmentData" timeout="5">
<set-url>https://enrichment.internal/data</set-url>
<set-method>GET</set-method>
<set-header name="X-Api-Key" exists-action="override">
<value>{{EnrichmentApiKey}}</value>
</set-header>
<!-- Explicitly pass YOUR correlation ID — not copied from client -->
<set-header name="Request-Id" exists-action="override">
<value>@(context.Variables.GetValueOrDefault<string>("requestId"))</value>
</set-header>
<!-- No Authorization forwarded — internal service uses its own API key -->
</send-request>
Step 3: Propagate Correlation ID to Backend
<!-- Before base — make sure the backend also gets your correlation ID -->
<set-header name="Request-Id" exists-action="override">
<value>@(context.Variables.GetValueOrDefault<string>("requestId"))</value>
</set-header>
<base />
Step 4: Fire-and-Forget Audit With Full Trace
<send-request mode="new" response-variable-name="audit" timeout="2" ignore-error="true">
<set-url>https://audit.internal/events</set-url>
<set-method>POST</set-method>
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<set-header name="X-Api-Key" exists-action="override">
<value>{{AuditApiKey}}</value>
</set-header>
<set-header name="Request-Id" exists-action="override">
<value>@(context.Variables.GetValueOrDefault<string>("requestId"))</value>
</set-header>
<set-body>@{
return new JObject(
new JProperty("requestId", context.Variables.GetValueOrDefault<string>("requestId")),
new JProperty("thirdPartyRequestId", context.Variables.GetValueOrDefault<string>("thirdPartyRequestId", "")),
new JProperty("api", context.Api.Name),
new JProperty("operation", context.Operation.Name),
new JProperty("subscriptionId", context.Subscription?.Id ?? "anonymous"),
new JProperty("clientIp", context.Request.IpAddress),
new JProperty("method", context.Request.Method),
new JProperty("timestamp", DateTime.UtcNow.ToString("o"))
).ToString();
}</set-body>
</send-request>
Complete Policy Putting It All Together
<policies>
<inbound>
<!-- Step 1: Capture third-party Request-Id and establish internal correlation ID -->
<set-variable name="thirdPartyRequestId" value="@{
var tpId = context.Request.Headers.GetValueOrDefault("Request-Id", "");
if (string.IsNullOrEmpty(tpId)) {
tpId = context.Request.Headers.GetValueOrDefault("X-Request-Id", "");
}
return tpId;
}" />
<set-variable name="requestId" value="@{
var existing = context.Request.Headers.GetValueOrDefault("Request-Id", "");
return string.IsNullOrEmpty(existing) ? Guid.NewGuid().ToString() : existing;
}" />
<set-header name="Request-Id" exists-action="override">
<value>@(context.Variables.GetValueOrDefault<string>("requestId"))</value>
</set-header>
<!-- Step 2: Enrich with internal data — mode="new", no leakage -->
<send-request mode="new" response-variable-name="userProfile" timeout="5" ignore-error="true">
<set-url>@("https://users.internal/profile/" + context.Subscription.Id)</set-url>
<set-method>GET</set-method>
<set-header name="X-Api-Key" exists-action="override">
<value>{{UserServiceApiKey}}</value>
</set-header>
<set-header name="Request-Id" exists-action="override">
<value>@(context.Variables.GetValueOrDefault<string>("requestId"))</value>
</set-header>
</send-request>
<!-- Step 3: Inject enriched data into backend request -->
<set-variable name="userTier" value="@{
try {
var resp = (IResponse)context.Variables["userProfile"];
if (resp?.StatusCode == 200) {
return resp.Body.As<JObject>()["tier"]?.ToString() ?? "free";
}
} catch {}
return "free";
}" />
<set-header name="X-User-Tier" exists-action="override">
<value>@(context.Variables.GetValueOrDefault<string>("userTier"))</value>
</set-header>
<!-- Step 4: Audit log — fire-and-forget, mode="new" -->
<send-request mode="new" response-variable-name="auditIgnored" timeout="2" ignore-error="true">
<set-url>https://audit.internal/events</set-url>
<set-method>POST</set-method>
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<set-header name="X-Api-Key" exists-action="override">
<value>{{AuditApiKey}}</value>
</set-header>
<set-header name="Request-Id" exists-action="override">
<value>@(context.Variables.GetValueOrDefault<string>("requestId"))</value>
</set-header>
<set-body>@{
return new JObject(
new JProperty("requestId", context.Variables.GetValueOrDefault<string>("requestId")),
new JProperty("thirdPartyRequestId", context.Variables.GetValueOrDefault<string>("thirdPartyRequestId", "")),
new JProperty("event", "inbound"),
new JProperty("api", context.Api.Name),
new JProperty("operation", context.Operation.Name),
new JProperty("subscriptionId", context.Subscription?.Id ?? "anonymous"),
new JProperty("userTier", context.Variables.GetValueOrDefault<string>("userTier")),
new JProperty("clientIp", context.Request.IpAddress),
new JProperty("timestamp", DateTime.UtcNow.ToString("o"))
).ToString();
}</set-body>
</send-request>
<!-- Step 5: Forward correlation ID to backend -->
<set-header name="Request-Id" exists-action="override">
<value>@(context.Variables.GetValueOrDefault<string>("requestId"))</value>
</set-header>
<base />
</inbound>
<backend>
<base />
</backend>
<outbound>
<!-- Ensure correlation ID is in the response too -->
<set-header name="Request-Id" exists-action="override">
<value>@(context.Variables.GetValueOrDefault<string>("requestId"))</value>
</set-header>
<base />
</outbound>
<on-error>
<set-header name="Request-Id" exists-action="override">
<value>@(context.Variables.GetValueOrDefault<string>("requestId"))</value>
</set-header>
<base />
</on-error>
</policies>
Common Mistakes and How to Fix Them
Mistake 1: Using mode="copy" as the Default
<!-- ❌ Wrong — copy is not a safe default -->
<send-request mode="copy" response-variable-name="result" timeout="5">
<set-url>https://internal-service.com/api</set-url>
<set-method>POST</set-method>
<set-body>{"myPayload": true}</set-body>
<!-- Body is set, but ALL original headers still leak through -->
</send-request>
<!-- ✅ Correct — new is the safe default for all internal calls -->
<send-request mode="new" response-variable-name="result" timeout="5">
<set-url>https://internal-service.com/api</set-url>
<set-method>POST</set-method>
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<set-body>{"myPayload": true}</set-body>
</send-request>
Mistake 2: Forgetting Content-Type With mode="new"
With mode="copy", Content-Type is inherited. With mode="new" it is not — and if your body is JSON but Content-Type is missing, the downstream service may return 415 Unsupported Media Type.
<!-- ❌ Missing Content-Type — downstream may reject with 415 -->
<send-request mode="new" response-variable-name="result" timeout="5">
<set-url>https://service.internal/api</set-url>
<set-method>POST</set-method>
<set-body>{"data": "value"}</set-body>
</send-request>
<!-- ✅ Always explicitly set Content-Type with mode="new" -->
<send-request mode="new" response-variable-name="result" timeout="5">
<set-url>https://service.internal/api</set-url>
<set-method>POST</set-method>
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<set-body>{"data": "value"}</set-body>
</send-request>
Mistake 3: Not Handling ignore-error Correctly
For non-critical calls (audit, logging), always set ignore-error="true". For critical calls (token fetch, auth validation), leave it as false — you want the pipeline to fail if those calls fail.
<!-- Critical path — failure must stop the request -->
<send-request mode="new" response-variable-name="authToken" timeout="10" ignore-error="false">
<set-url>https://auth.internal/token</set-url>
...
</send-request>
<!-- Non-critical path — failure must NOT stop the request -->
<send-request mode="new" response-variable-name="auditIgnored" timeout="2" ignore-error="true">
<set-url>https://audit.internal/log</set-url>
...
</send-request>
Mistake 4: Setting Timeout Too High
Every send-request adds to the client's total wait time. Keep timeouts tight:
| Call Type | Recommended Timeout |
|---|---|
| Audit / logging (fire-and-forget) | 1–2 seconds |
| Token fetch (cached or fast IdP) | 5 seconds |
| Data enrichment (non-critical) | 3–5 seconds |
| Auth validation (critical path) | 5–10 seconds |
| Shadow traffic | 3 seconds |
Mistake 5: Not Casting the Response Variable
The response variable is typed as object in context. You must cast it to IResponse before reading:
<!-- ❌ Wrong — will throw at runtime -->
<set-variable name="status" value="@(context.Variables["myResponse"].StatusCode)" />
<!-- ✅ Correct — cast to IResponse first -->
<set-variable name="status" value="@(((IResponse)context.Variables["myResponse"]).StatusCode)" />
<!-- ✅ Also correct — safer with null check -->
<set-variable name="isOk" value="@{
var resp = context.Variables["myResponse"] as IResponse;
return resp != null && resp.StatusCode == 200;
}" />
Quick Reference
Attributes of send-request
| Attribute | Required | Description |
|---|---|---|
mode | Yes | new (blank request) or copy (clone of original) |
response-variable-name | Yes | Name of the context variable to store the response |
timeout | No | Seconds before the call times out (default: 60) |
ignore-error | No | true = swallow errors silently; false = fail the pipeline (default: false) |
Child Elements
| Element | Description |
|---|---|
<set-url> | The target URL — supports policy expressions |
<set-method> | HTTP method (GET, POST, PUT, DELETE, etc.) |
<set-header> | Add or override a header |
<set-body> | Set the request body — supports policy expressions |
Reading the Response
<!-- Status code -->
@(((IResponse)context.Variables["myVar"]).StatusCode)
<!-- Response header -->
@(((IResponse)context.Variables["myVar"]).Headers.GetValueOrDefault("X-Header", ""))
<!-- Response body as string -->
@(((IResponse)context.Variables["myVar"]).Body.As<string>())
<!-- Response body as JObject (JSON) -->
@(((IResponse)context.Variables["myVar"]).Body.As<JObject>())
Summary
The send-request policy is one of the most versatile tools in APIM — but mode="copy" silently causes context leakage, header pollution, and tracking corruption that is hard to diagnose.
The rules to remember:
mode="new"is the safe default for every internal, audit, enrichment, webhook, or token call. You start clean and explicitly control every header and body element.mode="copy"is only correct when you intentionally want to replicate the client's full request to another destination — shadow traffic, transparent proxying, A/B routing.- Always set
Content-Typeexplicitly when usingmode="new"with a body. - Always set
Request-Idexplicitly in everysend-requestso your trace chain stays intact across all services. - Use
ignore-error="true"only for non-critical paths. Auth and enrichment calls should fail loudly. - Keep timeouts short. Every
send-requestadds latency to the client's perceived response time.
When these rules are followed, send-request becomes a clean, observable, and secure mechanism for everything from audit logging to service orchestration inside your APIM gateway.