AmbiguousMatchException: The request matched multiple endpoints. means two or more endpoints in your ASP.NET Core app accept the same request, and routing has no rule to pick one. The log lists the competing routes right under the message. Give one of them a route constraint, so routing prefers it for the values it accepts, or move one under its own path segment.

In the test below two MapGet calls differed only in the name of the route parameter. Giving the first one an :int constraint and moving the second under its own path segment made both URLs answer 200.

The error

fail: Microsoft.AspNetCore.Server.Kestrel[13]
      Connection id "0HNPRRA5CORQR", Request id "0HNPRRA5CORQR:00000001": An unhandled exception was thrown by the application.
      Microsoft.AspNetCore.Routing.Matching.AmbiguousMatchException: The request matched multiple endpoints. Matches:

      HTTP: GET /orders/{id}
      HTTP: GET /orders/{number}
         at Microsoft.AspNetCore.Routing.Matching.DefaultEndpointSelector.ReportAmbiguity(Span`1 candidateState)
...

The client saw only HTTP/1.1 500 Internal Server Error with Content-Length: 0, because the app ran in the Production environment. The exception and the list of matches were in the server's console log.

Why it happens

The broken app mapped these two endpoints:

app.MapGet("/orders/{id}", (string id) => $"order {id}");
app.MapGet("/orders/{number}", (string number) => $"order number {number}");

The name inside the braces is only the name the value is bound to. Both templates are "/orders/ followed by any one segment", both are GET, and neither has a constraint, so the request GET /orders/42 matched both. The lines after Matches: are the route templates of the endpoints that tied, which is the quickest way to find them in a large project.

The fix

app.MapGet("/orders/{id:int}", (int id) => $"order {id}");
app.MapGet("/orders/by-number/{number}", (string number) => $"order number {number}");

With this change GET /orders/42 answered order 42 and GET /orders/by-number/A-1001 answered order number A-1001, both with 200. Each change was also run on its own. With only the :int constraint (second route still /orders/{number}), /orders/42 answered order 42 and /orders/A-1001 answered order number A-1001: both templates still match /orders/42, and routing picked the constrained one without an exception. With only the by-number segment (first route still /orders/{id} without a constraint), /orders/42 answered order 42 and /orders/by-number/A-1001 answered order number A-1001. If the two endpoints really did the same job, delete one of them instead.

How it was reproduced

A project from dotnet new web (net10.0) with the two MapGet calls above, started on http://127.0.0.1:15701 and called with curl.exe -s -D - -o logs/amb-body.txt http://127.0.0.1:15701/orders/42. .NET SDK 10.0.403, Microsoft.AspNetCore.App 10.0.14, Windows 11. The built FixLab.exe was started directly, without a launch profile, so the log reported Hosting environment: Production. The fixed version ran on port 15705.

Frequently asked

How do I find which endpoints are ambiguous in ASP.NET Core?
Read the server log, not the browser. Under 'Matches:' the exception lists each endpoint that matched, for example HTTP: GET /orders/{id} and HTTP: GET /orders/{number}.
Does renaming the route parameter fix AmbiguousMatchException?
No. In the test, /orders/{id} and /orders/{number} still tied, because the parameter name does not change which URLs a template matches. In the test, an :int constraint on one route made routing pick that route for /orders/42 although both still matched, and a different literal segment such as /orders/by-number/{number} separated the URLs; each worked on its own.
Why does the browser only show a 500 with no details?
The app ran in the Production environment, which returned an empty 500 response. The exception text with the list of matched endpoints was written to the server's console log.

More decoded errors in the Fixes category. For how the app's address and port were chosen, see Kestrel ports and URLs.