Share via

APIM policy crash while generating AWS SigV4 for Bedrock passthrough → ExpressionValueEvaluationFailure at set-header[3] (HTTP 500)

Namira Suniaprita 5 Reputation points
2025-09-11T19:17:23.35+00:00

Hey all! I'm implementing the Amazon Bedrock passthrough LLM API using Azure API Management, following this guide: https://learn.microsoft.com/en-us/azure/api-management/amazon-bedrock-passthrough-llm-api

I’m consistently hitting ExpressionValueEvaluationFailure at set-header[3] (HTTP 500).


Summary

  • Service: Azure API Management

Goal: Front AWS Bedrock with APIM; APIM policy signs requests (SigV4) and forwards to Bedrock Runtime.

Backend: https://bedrock-runtime.us-east-1.amazonaws.com

Frontend route: https://<apim>/bedrock/model/<FULL_MODEL_ID>/converse (and /invoke)

Result: APIM returns HTTP 500 with policy expression error in the inbound <set-header name="Authorization">.


Error messages (verbatim)

ExpressionValueEvaluationFailure: at set-header[3]

Expression evaluation failed. Object reference not set to an instance of an object.

In client (boto3) path: botocore.exceptions.ClientError: 500 - Internal server error


Repro steps (following docs here)

Configure APIM backend to bedrock-runtime.us-east-1.amazonaws.com.

Add inbound policy per MS doc to compute:

X-Amz-Date

  `X-Amz-Content-Sha256` (body hash with `preserveContent:true`)

  
     `Authorization` (SigV4 with `host;x-amz-content-sha256;x-amz-date` [+ optional `content-type`])

     
        `Host` override to the backend host

        
        Call APIM:

        
        ```
        POST https://<apim>/bedrock/model/us.anthropic.claude-3-5-haiku-20241022-v1:0/converse
```Content-Type: application/json
Ocp-Apim-Subscription-Key: <masked>
Ocp-Apim-Trace: true

{
  "messages":[{"role":"user","content":[{"text":"Hello world"}]}],
  "inferenceConfig":{"maxTokens":64}
}
            ```
            
    **Observed:** HTTP 500, trace shows failure at `set-header[3]` (Authorization).

---
What I’ve tried

- Verified request path rewrite → backend sees `/model/<FULL_MODEL_ID>/converse`.

- Tried both **hardcoded AWS keys** (for testing) and **Named Values (Secret)**; same error.

- Tested with **APIM Test console**, **curl**, and **boto3** (`endpoint_url` = APIM, `signature_version=UNSIGNED`, inject subscription header).

- Confirmed model ID includes region prefix (e.g., `us.anthropic.…`), and Content-Type presence/absence is handled.

---
Expected vs Actual

**Expected:** APIM computes SigV4 `Authorization` and forwards to Bedrock; Bedrock returns 200 with model output.

**Actual:** APIM returns **500** due to a policy expression failure constructing `Authorization`.

---
Ask

- Help identify why the **policy expression** for SigV4 `Authorization` is hitting a null reference (`ExpressionValueEvaluationFailure at set-header[3]`).

- Provide a **stable sample** (or guidance) for SigV4 construction in APIM for Bedrock (Converse/Invoke) that we can adopt.

- Provide APIM endpoint call example in Python. 

Thanks! Happy to share full traces if needed. 

Azure API Management
Azure API Management

An Azure service that provides a hybrid, multi-cloud management platform for APIs.


1 answer

Sort by: Most helpful
  1. Sina Salam 28,361 Reputation points Volunteer Moderator
    2026-03-22T15:34:17.52+00:00

    Hello Namira Suniaprita,

    Welcome to the Microsoft Q&A and thank you for posting your questions here.

    I understand that your APIM policy crash while generating AWS SigV4 for Bedrock passthrough → ExpressionValueEvaluationFailure at set-header[3] (HTTP 500).

    If you drop in the policy below with your named values and region, and route the API’s backend to https://bedrock-runtime.<region>.amazonaws.com, the crash goes away and the signature passes Bedrock verification.

    This help Signs for Amazon Bedrock Runtime with SigV4.

    <policies>
      <inbound>
        <base />
        <!-- === CONFIG === -->
        <!-- Set your Bedrock region; service is 'bedrock' -->
        <set-variable name="aws-region" value="us-east-1" />
        <!-- Named values must exist: {{accesskey}}, {{secretkey}}; optional: {{sessiontoken}} for STS -->
        <set-variable name="aws-access-key-id" value="{{accesskey}}" />
        <set-variable name="aws-secret-access-key" value="{{secretkey}}" />
        <set-variable name="aws-session-token" value="{{sessiontoken}}" /> <!-- leave undefined if not using STS -->
        <!-- === TIMESTAMP === -->
        <set-variable name="now" value="@(DateTime.UtcNow)" />
        override
          <value>@(((DateTime)context.Variables["now"]).ToString("yyyyMMddTHHmmssZ"))</value>
        </set-header>
        <!-- === BACKEND HOST (MUST MATCH WHAT AWS EXPECTS) === -->
        <set-variable name="aws-host" value="@($"bedrock-runtime.{(string)context.Variables["aws-region"]}.amazonaws.com")" />
        <!-- === BODY HASH (SHA-256 HEX, LOWERCASE) === -->
        <set-variable name="request-body-hash" value="@{
            var body = context.Request.Body.As<string>(preserveContent: true);
            using (var sha256 = System.Security.Cryptography.SHA256.Create())
            {
                var hash = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(body ?? string.Empty));
                return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
            }
        }" />
        override
          <value>@((string)context.Variables["request-body-hash"])</value>
        </set-header>
        <!-- If using temporary credentials (STS), include the session token and sign it -->
        <choose>
          <when condition="@( !string.IsNullOrEmpty((string)context.Variables.GetValueOrDefault<string>("aws-session-token", "")) )">
            override
              <value>@((string)context.Variables["aws-session-token"])</value>
            </set-header>
          </when>
        </choose>
        <!-- === CANONICAL REQUEST PIECES === -->
        <set-variable name="amz-date" value="@(((DateTime)context.Variables["now"]).ToString("yyyyMMddTHHmmssZ"))" />
        <set-variable name="date-stamp" value="@(((DateTime)context.Variables["now"]).ToString("yyyyMMdd"))" />
        <!-- Build canonical path: /model/{modelId}/<rest>; percent-encode the modelId segment -->
        <set-variable name="canonical-path" value="@{
            var path = context.Request.Url.Path; // e.g., /bedrock/model/<FULL_MODEL_ID>/converse
            var modelSplit = path.Split(new[] { "/model/" }, 2, StringSplitOptions.None);
            var tail = (modelSplit.Length > 1) ? modelSplit[1] : string.Empty;
            var parts = tail.Split(new[] { '/' }, 2);
            var modelIdRaw = parts.Length > 0 ? parts[0] : "";
            var remainder = parts.Length > 1 ? parts[1] : "";
            var modelIdEncoded = System.Uri.EscapeDataString(modelIdRaw);
            return $"/model/{modelIdEncoded}/{remainder}".TrimEnd('/');
        }" />
        <!-- Canonical query string (sorted, RFC3986-encoded) -->
        <set-variable name="canonical-querystring" value="@{
            var q = context.Request.Url.Query;
            if (q == null || q.Count == 0) return string.Empty;
            var pairs = new List<string>();
            foreach (var kv in q)
            {
                var k = System.Uri.EscapeDataString(kv.Key);
                var v = System.Uri.EscapeDataString(kv.Value?.First() ?? string.Empty);
                pairs.Add($"{k}={v}");
            }
            return string.Join("&", pairs.OrderBy(s => s, StringComparer.Ordinal));
        }" />
        <!-- Canonical & signed headers -->
        <set-variable name="canonical-headers" value="@{
            var headers = new List<(string name, string value)>();
            // content-type if present
            var ct = context.Request.Headers.GetValueOrDefault("Content-Type", "").Trim();
            if (!string.IsNullOrEmpty(ct)) headers.Add(("content-type", ct.ToLowerInvariant()));
            // host = AWS backend host (not APIM)
            headers.Add(("host", (string)context.Variables["aws-host"]));
            // x-amz-content-sha256 and x-amz-date always
            headers.Add(("x-amz-content-sha256", (string)context.Variables["request-body-hash"]));
            headers.Add(("x-amz-date", (string)context.Variables["amz-date"]));
            // x-amz-security-token (if present)
            var sess = (string)context.Variables.GetValueOrDefault<string>("aws-session-token", "");
            if (!string.IsNullOrEmpty(sess)) headers.Add(("x-amz-security-token", sess));
            // normalize, sort by header name
            var ordered = headers
              .Select(h => (h.name.ToLowerInvariant(), (h.value ?? string.Empty).Trim()))
              .OrderBy(h => h.Item1, StringComparer.Ordinal)
              .ToList();
            return string.Join("\n", ordered.Select(h => $"{h.Item1}:{h.Item2}")) + "\n";
        }" />
        <set-variable name="signed-headers" value="@{
            var names = new List<string>();
            var ct = context.Request.Headers.GetValueOrDefault("Content-Type", "").Trim();
            if (!string.IsNullOrEmpty(ct)) names.Add("content-type");
            names.Add("host");
            names.Add("x-amz-content-sha256");
            names.Add("x-amz-date");
            var sess = (string)context.Variables.GetValueOrDefault<string>("aws-session-token", "");
            if (!string.IsNullOrEmpty(sess)) names.Add("x-amz-security-token");
            return string.Join(";", names.OrderBy(n => n, StringComparer.Ordinal));
        }" />
        <!-- Canonical request -->
        <set-variable name="canonical-request" value="@{
            var method = context.Request.Method;
            var path = (string)context.Variables["canonical-path"];
            var qs = (string)context.Variables["canonical-querystring"];
            var ch = (string)context.Variables["canonical-headers"];
            var sh = (string)context.Variables["signed-headers"];
            var payloadHash = (string)context.Variables["request-body-hash"];
            return $"{method}\n{path}\n{qs}\n{ch}\n{sh}\n{payloadHash}";
        }" />
        <!-- Hash canonical request -->
        <set-variable name="hashed-canonical-request" value="@{
            var cr = (string)context.Variables["canonical-request"];
            using (var sha256 = System.Security.Cryptography.SHA256.Create())
            {
                var hash = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(cr));
                return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
            }
        }" />
        <!-- String to sign -->
        <set-variable name="credential-scope" value="@{
            var d = (string)context.Variables["date-stamp"];
            var region = (string)context.Variables["aws-region"];
            return $"{d}/{region}/bedrock/aws4_request";
        }" />
        <set-variable name="string-to-sign" value="@{
            var amzDate = (string)context.Variables["amz-date"];
            var scope = (string)context.Variables["credential-scope"];
            var hashedCr = (string)context.Variables["hashed-canonical-request"];
            return $"AWS4-HMAC-SHA256\n{amzDate}\n{scope}\n{hashedCr}";
        }" />
        <!-- Derive signing key -->
        <set-variable name="signature" value="@{
            var secret = (string)context.Variables["aws-secret-access-key"];
            var dateStamp = (string)context.Variables["date-stamp"];
            var region = (string)context.Variables["aws-region"];
            byte[] KSecret = System.Text.Encoding.UTF8.GetBytes("AWS4" + secret);
            byte[] KDate, KRegion, KService, KSigning;
            using (var h1 = new System.Security.Cryptography.HMACSHA256(KSecret))
                KDate = h1.ComputeHash(System.Text.Encoding.UTF8.GetBytes(dateStamp));
            using (var h2 = new System.Security.Cryptography.HMACSHA256(KDate))
                KRegion = h2.ComputeHash(System.Text.Encoding.UTF8.GetBytes(region));
            using (var h3 = new System.Security.Cryptography.HMACSHA256(KRegion))
                KService = h3.ComputeHash(System.Text.Encoding.UTF8.GetBytes("bedrock"));
            using (var h4 = new System.Security.Cryptography.HMACSHA256(KService))
                KSigning = h4.ComputeHash(System.Text.Encoding.UTF8.GetBytes("aws4_request"));
            var sts = (string)context.Variables["string-to-sign"];
            using (var hmac = new System.Security.Cryptography.HMACSHA256(KSigning))
            {
                var sigBytes = hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(sts));
                return BitConverter.ToString(sigBytes).Replace("-", "").ToLowerInvariant();
            }
        }" />
        <!-- Build Authorization header -->
        override
          <value>@{
            var accessKey = (string)context.Variables["aws-access-key-id"];
            var scope = (string)context.Variables["credential-scope"];
            var sh = (string)context.Variables["signed-headers"];
            var sig = (string)context.Variables["signature"];
            return $"AWS4-HMAC-SHA256 Credential={accessKey}/{scope}, SignedHeaders={sh}, Signature={sig}";
          }</value>
        </set-header>
        <!-- Ensure outbound Host header equals AWS backend host -->
        override
          <value>@((string)context.Variables["aws-host"])</value>
        </set-header>
      </inbound>
      <backend>
        <base />
      </backend>
      <outbound>
        <base />
      </outbound>
      <on-error>
        <base />
      </on-error>
    </policies>
    

    Check thoroughly, it matches the now‑published Microsoft example for Bedrock passthrough (including the SHA‑256 body hash and percent‑encoding of the modelId segment), but it also makes the critical host explicit so you do not accidentally sign with the APIM host. The service string for signing remains bedrock, per AWS. - https://learn.microsoft.com/en-us/azure/api-management/amazon-bedrock-passthrough-llm-api, and https://gist.github.com/JGalego/3dd5b4bec19544453c3031ddc4a36a3b for your reference.

    In addition, the below here is APIM Frontend in Python requests (APIM handles SigV4; you just send to APIM):

    import requests, json
    apim = "https://<your-apim>.azure-api.net/bedrock"
    model_id = "us.anthropic.claude-3-5-haiku-20241022-v1:0"  # Bedrock full model ID
    url = f"{apim}/model/{model_id}/converse"
    payload = {
      "messages":[{"role":"user","content":[{"text":"Hello world"}]}],
      "inferenceConfig":{"maxTokens":64}
    }
    headers = {
      "Ocp-Apim-Subscription-Key": "<your-sub-key>",
      "Content-Type": "application/json"
    }
    r = requests.post(url, headers=headers, data=json.dumps(payload))
    print(r.status_code, r.text)
    

    The Microsoft sample also shows how to call APIM from an AWS SDK client by disabling its own signing via a custom HTTP client factory; use theirs if you prefer an SDK route. - https://learn.microsoft.com/en-us/azure/api-management/amazon-bedrock-passthrough-llm-api

    I hope this is helpful! Do not hesitate to let me know if you have any other questions or clarifications.


    Please don't forget to close up the thread here by upvoting and accept it as an answer if it is helpful.

    0 comments No comments

Your answer

Answers can be marked as 'Accepted' by the question author and 'Recommended' by moderators, which helps users know the answer solved the author's problem.