← Back to ArticlesAPI Management

APIM — Request Validation and Input Sanitization Policies

Implementing request validation, body schemas, header validation, and input sanitization with Azure APIM policies.

APIM — Request Validation and Input Sanitization Policies

Why Request Validation at the Gateway Matters

Your API gateway is the front door to your backend services. Every request that passes through without validation is a potential security vulnerability, a source of downstream errors, or a wasted compute cycle processing garbage data.

Without request validation at the APIM layer:

By validating at the gateway, you create a single enforcement point that protects all backend services consistently.


Architecture: Validation Flow

┌─────────────────────────────────────────────────────────────────────────┐
│                    REQUEST VALIDATION FLOW                              │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  Client Request                                                         │
│       │                                                                 │
│       ▼                                                                 │
│  ┌─────────────────────────────────────────────────────────────────┐    │
│  │  LAYER 1: Transport Validation                                  │    │
│  │  - Content-Type header present and correct                      │    │
│  │  - Content-Length within limits                                 │    │
│  │  - Required headers present (X-Client-Id, X-Request-Id)         │    │
│  └────────────────────────────────┬────────────────────────────────┘    │
│                                   │ Pass                                │
│                                   ▼                                     │
│  ┌─────────────────────────────────────────────────────────────────┐    │
│  │  LAYER 2: Schema Validation                                     │    │
│  │  - JSON body matches OpenAPI schema                             │    │
│  │  - Required fields present                                      │    │
│  │  - Data types correct (string, number, array)                   │    │
│  │  - String lengths within bounds                                 │    │
│  │  - Enum values valid                                            │    │
│  └────────────────────────────────┬────────────────────────────────┘    │
│                                   │ Pass                                │
│                                   ▼                                     │
│  ┌─────────────────────────────────────────────────────────────────┐    │
│  │  LAYER 3: Content Sanitization                                  │    │
│  │  - Strip HTML/script tags from string fields                    │    │
│  │  - Remove unexpected properties (additional properties)         │    │
│  │  - Normalize encoding                                           │    │
│  │  - Trim whitespace from identifiers                             │    │
│  └────────────────────────────────┬────────────────────────────────┘    │
│                                   │ Pass                                │
│                                   ▼                                     │
│  ┌─────────────────────────────────────────────────────────────────┐    │
│  │  LAYER 4: Business Rule Validation                              │    │
│  │  - Date ranges valid (start < end)                              │    │
│  │  - Amounts positive                                             │    │
│  │  - Cross-field dependencies satisfied                           │    │
│  └────────────────────────────────┬────────────────────────────────┘    │
│                                   │ Pass                                │
│                                   ▼                                     │
│                          Forward to Backend                             │
│                                                                         │
│  At ANY layer failure:                                                  │
│  → Return 400 Bad Request with specific error details                   │
│  → Log validation failure for monitoring                                │
│  → Increment validation failure metric                                  │
└─────────────────────────────────────────────────────────────────────────┘

Step 1: Built-in validate-content Policy

Azure APIM provides a built-in validate-content policy that validates request and response bodies against the OpenAPI schema defined in your API specification.

Basic Schema Validation

<policies>
    <inbound>
        <!-- Validate request body against the OpenAPI schema -->
        <validate-content unspecified-content-type-action="prevent"
                          max-size="102400"
                          size-exceeded-action="prevent"
                          errors-variable-name="validationErrors">
            <content type="application/json" validate-as="json"
                     action="prevent"
                     allow-additional-properties="false" />
        </validate-content>
        <base />
    </inbound>
</policies>

Understanding the Attributes

AttributePurposeRecommended Value
unspecified-content-type-actionWhat to do if Content-Type doesn't match any ruleprevent — reject unknown content types
max-sizeMaximum request body size in bytes102400 (100KB) for most APIs
size-exceeded-actionWhat to do if body exceeds max-sizeprevent — reject oversized requests
errors-variable-nameVariable to store validation errorsUse for custom error responses
actionWhat to do on validation failureprevent (reject) or detect (log only)
allow-additional-propertiesAllow fields not in schemafalse — strict validation

Validation Actions Explained


Step 2: Header Validation

Validate that required headers are present and contain expected values before processing the request.

Required Headers Check

<policies>
    <inbound>
        <!-- Validate required headers -->
        <choose>
            <!-- Check X-Client-Id header -->
            <when condition="@(!context.Request.Headers.ContainsKey("X-Client-Id"))">
                <return-response>
                    <set-status code="400" reason="Bad Request" />
                    <set-header name="Content-Type" exists-action="override">
                        <value>application/json</value>
                    </set-header>
                    <set-body>{
    "error": "missing_required_header",
    "message": "The X-Client-Id header is required for all API requests.",
    "details": {
        "header": "X-Client-Id",
        "description": "A unique identifier for the calling application. Obtain this from the developer portal."
    },
    "documentation": "https://docs.example.com/api/authentication#client-id"
}</set-body>
                </return-response>
            </when>
        </choose>

        <!-- Validate X-Request-Id format (must be UUID) -->
        <choose>
            <when condition="@{
                var requestId = context.Request.Headers.GetValueOrDefault("X-Request-Id", "");
                if (string.IsNullOrEmpty(requestId)) return false;
                return !System.Text.RegularExpressions.Regex.IsMatch(requestId, 
                    @"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$");
            }">
                <return-response>
                    <set-status code="400" reason="Bad Request" />
                    <set-header name="Content-Type" exists-action="override">
                        <value>application/json</value>
                    </set-header>
                    <set-body>{
    "error": "invalid_header_format",
    "message": "X-Request-Id must be a valid UUID (e.g., 550e8400-e29b-41d4-a716-446655440000).",
    "provided": "@(context.Request.Headers.GetValueOrDefault("X-Request-Id", ""))"
}</set-body>
                </return-response>
            </when>
        </choose>

        <!-- Generate X-Request-Id if not provided -->
        <set-header name="X-Request-Id" exists-action="skip">
            <value>@(Guid.NewGuid().ToString())</value>
        </set-header>

        <base />
    </inbound>
</policies>

Content-Type Validation

<inbound>
    <!-- Only allow application/json for POST/PUT/PATCH -->
    <choose>
        <when condition="@(new[] { "POST", "PUT", "PATCH" }.Contains(context.Request.Method))">
            <choose>
                <when condition="@{
                    var contentType = context.Request.Headers.GetValueOrDefault("Content-Type", "");
                    return !contentType.Contains("application/json");
                }">
                    <return-response>
                        <set-status code="415" reason="Unsupported Media Type" />
                        <set-header name="Content-Type" exists-action="override">
                            <value>application/json</value>
                        </set-header>
                        <set-body>{
    "error": "unsupported_media_type",
    "message": "This API only accepts application/json content type for request bodies.",
    "provided": "@(context.Request.Headers.GetValueOrDefault("Content-Type", "not specified"))",
    "accepted": ["application/json"]
}</set-body>
                    </return-response>
                </when>
            </choose>
        </when>
    </choose>
    <base />
</inbound>

Step 3: Query Parameter Validation

Validate query parameters for type, range, and allowed values.

<inbound>
    <!-- Validate pagination parameters -->
    <choose>
        <!-- Validate 'page' parameter -->
        <when condition="@{
            var page = context.Request.Url.Query.GetValueOrDefault("page", "1");
            int pageNum;
            return !int.TryParse(page, out pageNum) || pageNum < 1 || pageNum > 10000;
        }">
            <return-response>
                <set-status code="400" reason="Bad Request" />
                <set-header name="Content-Type" exists-action="override">
                    <value>application/json</value>
                </set-header>
                <set-body>{
    "error": "invalid_parameter",
    "message": "The 'page' parameter must be an integer between 1 and 10000.",
    "parameter": "page",
    "provided": "@(context.Request.Url.Query.GetValueOrDefault("page", "null"))",
    "constraints": { "type": "integer", "minimum": 1, "maximum": 10000 }
}</set-body>
            </return-response>
        </when>

        <!-- Validate 'pageSize' parameter -->
        <when condition="@{
            var size = context.Request.Url.Query.GetValueOrDefault("pageSize", "20");
            int sizeNum;
            return !int.TryParse(size, out sizeNum) || sizeNum < 1 || sizeNum > 100;
        }">
            <return-response>
                <set-status code="400" reason="Bad Request" />
                <set-header name="Content-Type" exists-action="override">
                    <value>application/json</value>
                </set-header>
                <set-body>{
    "error": "invalid_parameter",
    "message": "The 'pageSize' parameter must be an integer between 1 and 100.",
    "parameter": "pageSize",
    "provided": "@(context.Request.Url.Query.GetValueOrDefault("pageSize", "null"))",
    "constraints": { "type": "integer", "minimum": 1, "maximum": 100 }
}</set-body>
            </return-response>
        </when>

        <!-- Validate 'sortBy' parameter against allowed values -->
        <when condition="@{
            var sortBy = context.Request.Url.Query.GetValueOrDefault("sortBy", "");
            if (string.IsNullOrEmpty(sortBy)) return false;
            var allowed = new[] { "name", "date", "price", "rating" };
            return !allowed.Contains(sortBy.ToLower());
        }">
            <return-response>
                <set-status code="400" reason="Bad Request" />
                <set-header name="Content-Type" exists-action="override">
                    <value>application/json</value>
                </set-header>
                <set-body>{
    "error": "invalid_parameter",
    "message": "The 'sortBy' parameter must be one of the allowed values.",
    "parameter": "sortBy",
    "provided": "@(context.Request.Url.Query.GetValueOrDefault("sortBy", ""))",
    "allowed_values": ["name", "date", "price", "rating"]
}</set-body>
            </return-response>
        </when>
    </choose>
    <base />
</inbound>

Step 4: JSON Body Validation with Custom Schema

For complex validation beyond what the built-in validate-content provides, use inline schema validation with C# expressions.

Custom Body Validation

<inbound>
    <!-- Read and validate the request body -->
    <set-variable name="requestBody" value="@(context.Request.Body.As<JObject>(preserveContent: true))" />

    <!-- Validate required fields and business rules -->
    <set-variable name="validationErrors" value="@{
        var errors = new List<string>();
        var body = (JObject)context.Variables["requestBody"];

        // Check required fields
        if (body["orderId"] == null || string.IsNullOrWhiteSpace(body["orderId"].ToString()))
            errors.Add("'orderId' is required and cannot be empty");

        if (body["customerId"] == null || string.IsNullOrWhiteSpace(body["customerId"].ToString()))
            errors.Add("'customerId' is required and cannot be empty");

        if (body["items"] == null || !body["items"].HasValues)
            errors.Add("'items' array is required and must contain at least one item");

        // Validate field formats
        if (body["email"] != null) {
            var email = body["email"].ToString();
            if (!System.Text.RegularExpressions.Regex.IsMatch(email, 
                @"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"))
                errors.Add("'email' must be a valid email address");
        }

        // Validate numeric ranges
        if (body["totalAmount"] != null) {
            decimal amount;
            if (!decimal.TryParse(body["totalAmount"].ToString(), out amount) || amount <= 0)
                errors.Add("'totalAmount' must be a positive number");
            if (amount > 1000000)
                errors.Add("'totalAmount' cannot exceed 1,000,000");
        }

        // Validate string lengths
        if (body["notes"] != null && body["notes"].ToString().Length > 500)
            errors.Add("'notes' cannot exceed 500 characters");

        // Validate items array
        if (body["items"] != null && body["items"].HasValues) {
            var items = (JArray)body["items"];
            if (items.Count > 50)
                errors.Add("'items' array cannot contain more than 50 items");

            for (int i = 0; i < items.Count; i++) {
                var item = items[i];
                if (item["productId"] == null)
                    errors.Add($"items[{i}].productId is required");
                if (item["quantity"] == null || (int)item["quantity"] <= 0)
                    errors.Add($"items[{i}].quantity must be a positive integer");
                if (item["quantity"] != null && (int)item["quantity"] > 9999)
                    errors.Add($"items[{i}].quantity cannot exceed 9999");
            }
        }

        // Validate date fields
        if (body["deliveryDate"] != null) {
            DateTime deliveryDate;
            if (!DateTime.TryParse(body["deliveryDate"].ToString(), out deliveryDate))
                errors.Add("'deliveryDate' must be a valid ISO 8601 date");
            else if (deliveryDate < DateTime.UtcNow)
                errors.Add("'deliveryDate' cannot be in the past");
        }

        return string.Join("|", errors);
    }" />

    <!-- Return validation errors if any -->
    <choose>
        <when condition="@(!string.IsNullOrEmpty((string)context.Variables["validationErrors"]))">
            <return-response>
                <set-status code="400" reason="Bad Request" />
                <set-header name="Content-Type" exists-action="override">
                    <value>application/json</value>
                </set-header>
                <set-body>@{
                    var errors = ((string)context.Variables["validationErrors"]).Split('|');
                    var errorArray = string.Join(",\n        ", errors.Select(e => $"\"{e}\""));
                    return $@"{{
    ""error"": ""validation_failed"",
    ""message"": ""Request body validation failed. Please fix the errors and retry."",
    ""errors"": [
        {errorArray}
    ],
    ""errorCount"": {errors.Length}
}}";
                }</set-body>
            </return-response>
        </when>
    </choose>

    <base />
</inbound>

Step 5: Input Sanitization

Even after validation, you should sanitize input to remove potentially dangerous content before forwarding to your backend.

Removing Dangerous Content

<inbound>
    <!-- Sanitize the request body -->
    <set-body>@{
        var body = context.Request.Body.As<JObject>(preserveContent: true);

        // Function to sanitize string values
        Func<string, string> sanitize = (input) => {
            if (string.IsNullOrEmpty(input)) return input;

            // Remove HTML tags (prevents XSS if data is rendered in a browser)
            input = System.Text.RegularExpressions.Regex.Replace(input, @"<[^>]*>", "");

            // Remove script-related content
            input = System.Text.RegularExpressions.Regex.Replace(input, 
                @"javascript:", "", System.Text.RegularExpressions.RegexOptions.IgnoreCase);
            input = System.Text.RegularExpressions.Regex.Replace(input, 
                @"on\w+\s*=", "", System.Text.RegularExpressions.RegexOptions.IgnoreCase);

            // Remove SQL injection patterns
            input = System.Text.RegularExpressions.Regex.Replace(input, 
                @"('|--|;|/\*|\*/|xp_|exec|execute|insert|select|delete|update|drop|alter|create)",
                "", System.Text.RegularExpressions.RegexOptions.IgnoreCase);

            // Trim whitespace
            input = input.Trim();

            return input;
        };

        // Sanitize all string properties recursively
        Action<JToken> sanitizeToken = null;
        sanitizeToken = (token) => {
            if (token.Type == JTokenType.Object) {
                foreach (var prop in ((JObject)token).Properties().ToList()) {
                    sanitizeToken(prop.Value);
                }
            } else if (token.Type == JTokenType.Array) {
                foreach (var item in (JArray)token) {
                    sanitizeToken(item);
                }
            } else if (token.Type == JTokenType.String) {
                var parent = token.Parent;
                if (parent is JProperty prop) {
                    prop.Value = sanitize(token.ToString());
                }
            }
        };

        sanitizeToken(body);

        // Remove any unexpected top-level properties
        var allowedProperties = new HashSet<string> { 
            "orderId", "customerId", "items", "totalAmount", 
            "email", "notes", "deliveryDate", "shippingAddress" 
        };
        
        foreach (var prop in body.Properties().ToList()) {
            if (!allowedProperties.Contains(prop.Name)) {
                prop.Remove();
            }
        }

        return body.ToString();
    }</set-body>

    <base />
</inbound>

Why Sanitize at the Gateway?

Attack TypeWhat It Looks LikeWhat Sanitization Does
XSS<script>alert('hacked')</script>Strips HTML tags
SQL Injection'; DROP TABLE users; --Removes SQL keywords and special chars
Command Injection; rm -rf /Removes shell metacharacters
Path Traversal../../etc/passwdNormalizes paths
Oversized Payload50MB JSON bodyRejected by max-size check

Step 6: Request Size and Rate Limiting

Protect your backend from oversized requests and abuse.

<inbound>
    <!-- Enforce maximum request body size -->
    <choose>
        <when condition="@(context.Request.Headers.GetValueOrDefault("Content-Length", "0") != "0" && 
                          int.Parse(context.Request.Headers.GetValueOrDefault("Content-Length", "0")) > 102400)">
            <return-response>
                <set-status code="413" reason="Payload Too Large" />
                <set-header name="Content-Type" exists-action="override">
                    <value>application/json</value>
                </set-header>
                <set-body>{
    "error": "payload_too_large",
    "message": "Request body exceeds the maximum allowed size of 100KB.",
    "maxSize": "102400 bytes",
    "provided": "@(context.Request.Headers.GetValueOrDefault("Content-Length", "unknown")) bytes"
}</set-body>
            </return-response>
        </when>
    </choose>

    <!-- Rate limit by client ID -->
    <rate-limit-by-key calls="100"
                       renewal-period="60"
                       counter-key="@(context.Request.Headers.GetValueOrDefault("X-Client-Id", "anonymous"))"
                       increment-condition="@(context.Response.StatusCode >= 200 && context.Response.StatusCode < 300)" />

    <base />
</inbound>

Step 7: Complete Production Policy

Here's a complete policy combining all validation layers:

<policies>
    <inbound>
        <!-- LAYER 1: Transport Validation -->
        <!-- Check required headers -->
        <choose>
            <when condition="@(!context.Request.Headers.ContainsKey("X-Client-Id"))">
                <return-response>
                    <set-status code="400" reason="Bad Request" />
                    <set-header name="Content-Type" exists-action="override">
                        <value>application/json</value>
                    </set-header>
                    <set-body>{"error": "missing_required_header", "message": "X-Client-Id header is required"}</set-body>
                </return-response>
            </when>
        </choose>

        <!-- Validate Content-Type for write operations -->
        <choose>
            <when condition="@(new[] {"POST","PUT","PATCH"}.Contains(context.Request.Method) && 
                             !context.Request.Headers.GetValueOrDefault("Content-Type","").Contains("application/json"))">
                <return-response>
                    <set-status code="415" reason="Unsupported Media Type" />
                    <set-header name="Content-Type" exists-action="override">
                        <value>application/json</value>
                    </set-header>
                    <set-body>{"error": "unsupported_media_type", "message": "Only application/json is accepted"}</set-body>
                </return-response>
            </when>
        </choose>

        <!-- Generate correlation ID -->
        <set-header name="X-Correlation-Id" exists-action="skip">
            <value>@(Guid.NewGuid().ToString())</value>
        </set-header>

        <!-- LAYER 2: Schema Validation (built-in) -->
        <validate-content unspecified-content-type-action="prevent"
                          max-size="102400"
                          size-exceeded-action="prevent"
                          errors-variable-name="schemaErrors">
            <content type="application/json" validate-as="json"
                     action="prevent"
                     allow-additional-properties="false" />
        </validate-content>

        <!-- LAYER 3: Rate Limiting -->
        <rate-limit-by-key calls="100"
                           renewal-period="60"
                           counter-key="@(context.Request.Headers.GetValueOrDefault("X-Client-Id", "anonymous"))" />

        <base />
    </inbound>

    <backend>
        <base />
    </backend>

    <outbound>
        <!-- Add correlation ID to response -->
        <set-header name="X-Correlation-Id" exists-action="override">
            <value>@(context.Request.Headers.GetValueOrDefault("X-Correlation-Id", ""))</value>
        </set-header>
        <base />
    </outbound>

    <on-error>
        <!-- Custom error response for validation failures -->
        <choose>
            <when condition="@(context.LastError.Source == "validate-content")">
                <return-response>
                    <set-status code="400" reason="Bad Request" />
                    <set-header name="Content-Type" exists-action="override">
                        <value>application/json</value>
                    </set-header>
                    <set-body>@{
                        var errors = context.Variables.GetValueOrDefault<string>("schemaErrors", "Schema validation failed");
                        return $@"{{
    ""error"": ""schema_validation_failed"",
    ""message"": ""Request body does not match the expected schema."",
    ""details"": ""{errors}"",
    ""correlationId"": ""{context.Request.Headers.GetValueOrDefault("X-Correlation-Id", "")}""
}}";
                    }</set-body>
                </return-response>
            </when>
        </choose>
    </on-error>
</policies>

Real-World Scenarios

Scenario 1: E-Commerce Product API

Validating product creation requests with complex business rules:

<set-variable name="productErrors" value="@{
    var errors = new List<string>();
    var body = context.Request.Body.As<JObject>(preserveContent: true);

    // Product name: required, 3-200 chars, no special characters
    var name = body["name"]?.ToString();
    if (string.IsNullOrWhiteSpace(name))
        errors.Add("Product name is required");
    else if (name.Length < 3 || name.Length > 200)
        errors.Add("Product name must be between 3 and 200 characters");

    // Price: required, positive, max 2 decimal places
    var price = body["price"];
    if (price == null)
        errors.Add("Price is required");
    else {
        decimal priceValue;
        if (!decimal.TryParse(price.ToString(), out priceValue) || priceValue <= 0)
            errors.Add("Price must be a positive number");
        else if (priceValue != Math.Round(priceValue, 2))
            errors.Add("Price cannot have more than 2 decimal places");
    }

    // SKU: required, alphanumeric with dashes, 5-20 chars
    var sku = body["sku"]?.ToString();
    if (string.IsNullOrWhiteSpace(sku))
        errors.Add("SKU is required");
    else if (!System.Text.RegularExpressions.Regex.IsMatch(sku, @"^[A-Za-z0-9\-]{5,20}$"))
        errors.Add("SKU must be 5-20 alphanumeric characters (dashes allowed)");

    // Category: must be from allowed list
    var category = body["category"]?.ToString();
    var allowedCategories = new[] { "electronics", "clothing", "home", "sports", "books" };
    if (!string.IsNullOrEmpty(category) && !allowedCategories.Contains(category.ToLower()))
        errors.Add($"Category must be one of: {string.Join(", ", allowedCategories)}");

    return string.Join("|", errors);
}" />

Scenario 2: Financial Transaction API

Strict validation for payment processing:

<set-variable name="paymentErrors" value="@{
    var errors = new List<string>();
    var body = context.Request.Body.As<JObject>(preserveContent: true);

    // Amount validation
    var amount = body["amount"];
    if (amount == null) {
        errors.Add("amount is required");
    } else {
        decimal amountValue;
        if (!decimal.TryParse(amount.ToString(), out amountValue))
            errors.Add("amount must be a valid decimal number");
        else if (amountValue <= 0)
            errors.Add("amount must be greater than zero");
        else if (amountValue > 50000)
            errors.Add("amount cannot exceed 50,000 per transaction");
    }

    // Currency validation (ISO 4217)
    var currency = body["currency"]?.ToString();
    var validCurrencies = new[] { "USD", "EUR", "GBP", "CAD", "AUD", "JPY" };
    if (string.IsNullOrEmpty(currency))
        errors.Add("currency is required");
    else if (!validCurrencies.Contains(currency.ToUpper()))
        errors.Add($"currency must be a valid ISO 4217 code: {string.Join(", ", validCurrencies)}");

    // Idempotency key (required for payment APIs)
    var idempotencyKey = context.Request.Headers.GetValueOrDefault("Idempotency-Key", "");
    if (string.IsNullOrEmpty(idempotencyKey))
        errors.Add("Idempotency-Key header is required for payment operations");
    else if (idempotencyKey.Length < 16 || idempotencyKey.Length > 64)
        errors.Add("Idempotency-Key must be between 16 and 64 characters");

    return string.Join("|", errors);
}" />

Scenario 3: File Upload Metadata Validation

<set-variable name="uploadErrors" value="@{
    var errors = new List<string>();
    var body = context.Request.Body.As<JObject>(preserveContent: true);

    // Filename validation
    var filename = body["filename"]?.ToString();
    if (string.IsNullOrWhiteSpace(filename))
        errors.Add("filename is required");
    else {
        // Check for path traversal
        if (filename.Contains("..") || filename.Contains("/") || filename.Contains("\\"))
            errors.Add("filename cannot contain path separators or '..'");
        // Check allowed extensions
        var allowedExtensions = new[] { ".pdf", ".jpg", ".png", ".docx", ".xlsx" };
        var ext = System.IO.Path.GetExtension(filename).ToLower();
        if (!allowedExtensions.Contains(ext))
            errors.Add($"File type '{ext}' is not allowed. Accepted: {string.Join(", ", allowedExtensions)}");
    }

    // File size (metadata check - actual size validated separately)
    var fileSize = body["fileSize"];
    if (fileSize != null) {
        long size;
        if (long.TryParse(fileSize.ToString(), out size) && size > 10485760)
            errors.Add("File size cannot exceed 10MB (10,485,760 bytes)");
    }

    return string.Join("|", errors);
}" />

Testing Your Validation Policies

Test Invalid Requests

# Test missing required header
curl -X POST "https://my-apim.azure-api.net/api/orders" \
  -H "Content-Type: application/json" \
  -d '{"orderId": "123"}' \
  -w "\nHTTP Status: %{http_code}\n"
# Expected: 400 - Missing X-Client-Id header

# Test invalid Content-Type
curl -X POST "https://my-apim.azure-api.net/api/orders" \
  -H "X-Client-Id: test-app" \
  -H "Content-Type: text/plain" \
  -d 'not json' \
  -w "\nHTTP Status: %{http_code}\n"
# Expected: 415 - Unsupported Media Type

# Test invalid body (missing required fields)
curl -X POST "https://my-apim.azure-api.net/api/orders" \
  -H "X-Client-Id: test-app" \
  -H "Content-Type: application/json" \
  -d '{"orderId": ""}' \
  -w "\nHTTP Status: %{http_code}\n"
# Expected: 400 - Validation errors

# Test oversized payload
dd if=/dev/zero bs=1024 count=200 | base64 | \
curl -X POST "https://my-apim.azure-api.net/api/orders" \
  -H "X-Client-Id: test-app" \
  -H "Content-Type: application/json" \
  -d @- \
  -w "\nHTTP Status: %{http_code}\n"
# Expected: 413 - Payload Too Large

# Test XSS attempt
curl -X POST "https://my-apim.azure-api.net/api/orders" \
  -H "X-Client-Id: test-app" \
  -H "Content-Type: application/json" \
  -d '{"orderId": "123", "notes": "<script>alert(1)</script>"}' \
  -w "\nHTTP Status: %{http_code}\n"
# Expected: 200 but with sanitized body (script tags removed)

Monitoring Validation Failures

KQL Queries

// Track validation failures over time
ApiManagementGatewayLogs
| where TimeGenerated > ago(24h)
| where ResponseCode == 400
| summarize ValidationFailures = count() by bin(TimeGenerated, 1h), ApiId
| render timechart
// Identify clients sending invalid requests
ApiManagementGatewayLogs
| where TimeGenerated > ago(7d)
| where ResponseCode in (400, 413, 415)
| extend ClientId = tostring(parse_json(RequestHeaders)["X-Client-Id"])
| summarize FailureCount = count() by ClientId, ResponseCode
| order by FailureCount desc
| take 20

Best Practices

PracticeWhyImplementation
Validate at the gatewaySingle enforcement pointUse APIM policies
Return specific error messagesBetter developer experienceInclude field name, constraint, and provided value
Use detect mode during migrationAvoid breaking existing clientsSwitch to prevent after monitoring
Sanitize even after validationDefense in depthStrip HTML, SQL keywords
Log validation failuresIdentify problematic clientsUse Azure Monitor
Version your schemasAllow gradual migrationDifferent validation per API version
Set appropriate size limitsPrevent resource exhaustion100KB for most APIs
Include correlation IDsEnable request tracingGenerate if not provided

Summary

Request validation at the API gateway provides:

Implement validation in layers: transport (headers, content-type), schema (structure, types), content (sanitization), and business rules (cross-field logic). Each layer catches different categories of invalid input.


Azure Integration Hub — API Management