Skip to content

Fix AmbiguousMatchException between PatchJson and PatchFhir methods #5176

@brendankowitz

Description

@brendankowitz

Problem

PATCH requests without a Content-Type header or with an empty body cause an AmbiguousMatchException and return HTTP 500.

Error Details

Microsoft.AspNetCore.Routing.Matching.AmbiguousMatchException: 
The request matched multiple endpoints. Matches: 
  FhirController.PatchJson (Microsoft.Health.Fhir.Api)
  FhirController.PatchFhir (Microsoft.Health.Fhir.Api)

Root Cause

FhirController has two PATCH methods with identical routes but different Consumes attributes:

Method 1: PatchJson (~line 484)

[HttpPatch]
[Route(KnownRoutes.ResourceTypeById)]
[Consumes("application/json-patch+json")]
public async Task<IActionResult> PatchJson(
    [FromBody] JsonPatchDocument<Resource> patch,
    ...)

Method 2: PatchFhir (~line 531)

[HttpPatch]
[Route(KnownRoutes.ResourceTypeById)]
[Consumes("application/fhir+json")]
public async Task<IActionResult> PatchFhir(
    [FromBody] Parameters parameters,
    ...)

ASP.NET Core evaluates route matching before applying Consumes constraints and before model binding. When a PATCH request arrives without a Content-Type header or with an empty body, the router cannot distinguish between these methods.

Reproduction

PATCH https://{{hostname}}/Patient/example
(No Content-Type header, no body)

Result: HTTP 500 with AmbiguousMatchException

Impact

  • Users receive HTTP 500 instead of proper validation error
  • Occurs with testing tools (PostmanRuntime) or malformed requests
  • Low frequency but poor user experience

Recommended Solution

Mark [FromBody] parameters as required to fail early with a clear error instead of ambiguous match:

Option 1: Use EmptyBodyBehavior.Disallow (Preferred)

public async Task<IActionResult> PatchJson(
    [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Disallow)] JsonPatchDocument<Resource> patch,
    ...)

public async Task<IActionResult> PatchFhir(
    [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Disallow)] Parameters parameters,
    ...)

This approach:

  • ✅ Fails early during model binding (before routing ambiguity)
  • ✅ Returns HTTP 400 automatically
  • ✅ Minimal code change
  • ✅ Leverages built-in ASP.NET Core functionality

Option 2: Custom Validation Middleware

Add middleware to reject PATCH requests with empty bodies before routing.

Option 3: Explicit Empty Body Handling

Handle the empty body case explicitly in both methods and return HTTP 400.

Testing Checklist

  • PATCH without Content-Type → HTTP 400 Bad Request
  • PATCH with empty body → HTTP 400 Bad Request
  • Error message indicates Content-Type and body are required
  • PATCH with Content-Type: application/json-patch+json + valid body → routes to PatchJson
  • PATCH with Content-Type: application/fhir+json + valid body → routes to PatchFhir
  • No AmbiguousMatchException in telemetry after deployment
  • E2E tests cover all scenarios
  • Integration tests verify routing and validation behavior
  • Unit tests verify EmptyBodyBehavior.Disallow behavior

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions