Receiving 405 when expecting 404's when route matching occurs. Only defined pattern is a single route with a complex segment.
Take a basic ASP.NET Core Web Api for this spike example (I used visual studio's out of the box "Weather Forecast") and tweak the controller so that the single endpoint is a POST with a template of {id}:bar on a controller whose route is foo.
Ergo, only one endpoint is defined in the whole project: POST foo/{id}:bar
Now, when I run the above api locally and use Postman to hit it with different requests varying only the verb and route I get the returned status';
/foo/123:bar
/foo/literal
GET
405 (expected)
405 (why?)
POST
200 (expected)
404 (expected)
DELETE
405 (expected)
405 (why?)
I expected all verbs for /foo/literal to return a 404 because the only route that can be matched is one that has two segments: foo and {id}:bar and the latter segment is a complex segment with a parameter part followed by a literal part. Said literal part is not suffixed to this route, ergo, it should not be a match and surely 404's should be returned?
So is this
a) a bug in route matching
b) a mistake in my understanding, and are those 405's for `/foo/literal' in fact correct?
Below is the only file I changed from the example project, it is the file "WeatherForecastController"
using Microsoft.AspNetCore.Mvc;
namespace SpikeExample.Controllers
{
[ApiController]
[Route("foo")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
[HttpPost("{id}:bar")]
public IEnumerable<WeatherForecast> Get()
{
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
}
}
I created an issue in the dotnet/aspnetcore repo and I received the below reply:
I believe this happens because we consider the HttpMethod before we evaluate the constraint and as a result, we produce a 405, since for us complex segments are just parameters with constraints.
I believe this is very much by design as we try to delay evaluating constraints as much as possible as they are expensive.
In this case, our HttpMethodMatcherPolicy sees the endpoint candidate and determines that it can't satisfy the methods but that there are other endpoints for other methods and that's why you see the 405
Related
I have an ASP.Net Core WebAPI, I got below requirement as
I have 2 methods to handle HTTP GET requests, the first one for GetCustomer by id(string) and the other one to GetCustomer by email(string).
//GET : api/customers/2913a1ad-d990-412a-8e30-dbe464c2a85e
[HttpGet("{id}")]
public async Task<ActionResult<Customer>> GetCustomer([FromRoute]string id)
{
}
// GET: api/customers/myemail#gmail.com
[HttpGet("{name}")]
public async Task<ActionResult<Customer>> GetCustomerByEmail([FromRoute]string email)
{
}
when I try to this Endpoint, I get exception as:
Microsoft.AspNetCore.Routing.Matching.AmbiguousMatchException: The request matched multiple endpoints.
Which is quite Obivious & understood.
It could be easily addressed by appending/prepending some string in the route like
/api/customer/{id}
/api/customer/emai/{id}
However not so convinced with this apporach however on googling I get the below SO link
How to handle multiple endpoints in ASP.Net Core 3 Web API properly
But this guy has one parameter as int while the other as string so route constraint rescued.
however, I see some people posting/suggesting on the same post to add [Route("")] but I didn't get how this attribute is useful ?
afaik, Route("") and HTTPGet("") //any HttpVerb serves the same purpose?
Anyways how could I handle my requirement elegantly?
You can add route constraints to disambiguate between the two routes:
[HttpGet("{id:guid}")]
[HttpGet("{name}")]
You could also create your own email constraint or use regex(.) constraint for the email parameter.
I gonna use these attributes on my controller:
[SwaggerResponse((int)HttpStatusCode.OK, typeof(GetAutotopupConfigurationResponse))]
[SwaggerResponse((int)HttpStatusCode.BadRequest, typeof(ErrorResponse), BadRequestMessage)]
[SwaggerResponse((int)HttpStatusCode.Unauthorized, typeof(ErrorResponse), InvalidCredentiasMessage)]
[SwaggerResponse((int)HttpStatusCode.Forbidden, typeof(ErrorResponse), UserNoRightsMessage)]
[SwaggerResponse((int)HttpStatusCode.NotFound, typeof(ErrorResponse), AutopopupNotFoundMessage)]
[SwaggerResponse((int)HttpStatusCode.InternalServerError, typeof(ErrorResponse), InternalServerErrorMessage)]
How do I simplify the logic and reduce code ammount or make it more flexible somehow?
EDIT This answer applies to Asp.Net-Core but may be useful for this question too.
If you're using Swashbuckle you can use an IOperationFilter and Reflection to target specific endpoints and programmatically apply the responses.
It's possible to use an IOperationFilter to apply InternalServerError to all endpoints in your service. Below is an example:
public class ServerErrorResponseOperationFilter : IOperationFilter
{
// Applies the specified operation. Adds 500 ServerError to Swagger documentation for all endpoints
public void Apply(Operation operation, OperationFilterContext context)
{
// ensure we are filtering on controllers
if (context.MethodInfo.DeclaringType.BaseType.BaseType == typeof(ControllerBase)
|| context.MethodInfo.ReflectedType.BaseType == typeof(Controller))
{
operation.Responses.Add("500", new Response { Description = "Server Error" });
}
}
}
You need to set Swagger to use these filters. You can do so by adding in the setup:
services.AddSwaggerGen(swag =>
{
swag.SwaggerDoc("v1", new Info { Title = "Docs", Version = "v1" });
// add swagger filters to document default responses
swag.OperationFilter<ServerErrorResponseOperationFilter>();
});
You can use other filters to apply 401 Unauthorized, 403 Forbidden, etc. You can even use Reflection to add 201 Created for actions decorated with [HttpPost] and you could do something similar for other Http attributes.
If you have filters for 401, 403 and 500 that will tidy up your controller slightly. You will still need to add attributes for certain methods that can't be dealt with by Reflection. With this method I find I only need to add one or 2 attributes, typically [ProcudesResponseType((int)HttpStatusCode.BadRequest)] and [ProcudesResponseType((int)HttpStatusCode.NotFound)].
My application is an ASP.NET Core 1.0 Web API.
[HttpGet("{someData:MinLength(5):MaxLength(5)}")]
[Produces("application/json")]
public async Task<IActionResult> GetSomeData(string someData)
{
return this.Ok(JsonConvert.SerializeObject("Data is: " + someData));
}
So when i pass a parameter with the length 6, the controller returns the Response Code 404 and the Response body no content because the parameter doesnt have the length 5.
This information is pretty useless for me, is there a way to return a more usefull error message?
I know i could just hardcode the error message in every controller like that:
[HttpGet("{someData}")]
[Produces("application/json")]
public async Task<IActionResult> GetSomeData(string someData)
{
if (someData.Length != 5)
{
return this.StatusCode(404, JsonConvert.SerializeObject("The data has to be 5 digits long."));
}
return this.Ok(JsonConvert.SerializeObject("Data is: " + someData));
}
The problem about this is, I have many controllers in my application and I dont want to validate the parameters everytime.
Is there a way the controller returns a more usefull Responsebody by itself? Or do I really have to add a validation method in each controller and forget about the [HttpGet] parameters and its error return?
Thank you very much
Route constraints are used to restrict which requested paths make it to a given action / route destination. They're not meant to perform model validation - that's done later and through a separate mechanism. If the request doesn't conform to your route constraints, it simply won't match that route. If it doesn't match any route, you'll get a 404.
As you noted in your own answer, this is covered in the routing docs: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/routing#route-template-reference
Alright I found an answer:
It's not possible to get better error messages.
Warning
Avoid using constraints for input validation, because doing so means
that invalid input will result in a 404 (Not Found) instead of a 400
with an appropriate error message. Route constraints should be used to
disambiguate between similar routes, not to validate the inputs for a
particular route.
found here
I am using Web API 2 and am seeing unexpected routing behavior when the URL ends with a forward slash. My Web API config looks like this.
public void Configuration(IAppBuilder appBuilder)
{
HttpConfiguration webApiConfig = new HttpConfiguration();
webApiConfig.MapHttpAttributeRoutes();
webApiConfig.EnsureInitialized();
appBuilder.UseWebApi(webApiConfig);
}
In WebApp.Start() I pass the url http://host:port/configuration.
I have two routes setup.
[Route("~/")]
public object GetResourcesList()
{
return new List<string>() { "Resource 1", "Resource 2", "Resource n" };
}
[Route("~/{resourceName}")]
public object GetResource(string resourceName)
{
return "Resource " + resourceName;
}
If I make a GET request to http://host:port/configuration/ it calls GetResourceList() as expected.
If I make a GET request to http://host:port/configuration (no forward slash at the end) it calls GetResource() and returns "Resource configuration". I would expect it to the call GetResourceList().
Why would it be calling GetResource() in the second example? And why would it pass configuration as the resourceName when WebApp.Start should be listening on http://host:port/configuration and not http://host:port?
I have also tried [Route("")] and [Route("{resourceName}")] for GetResourceList() and GetResource() respectively and saw the same behavior.
Thanks for the help.
EDIT
I also just tried listening on http://host:port/configuration/ (with a slash at the end). No difference in behavior.
I haven't messed with the OWIN self hosting stuff, but my guess is that it's listening on the URI you're providing but still passing in the entire URL, ie, the path that the Route picks up is not relative to the URI you're providing.
I would change your attribute routes from
[Route("~/")]
and
[Route("~/{resourceName}")]
to
[Route("configuration")]
and
[Route("configuration/{resourceName}")]
or use a route prefix of
[RoutePrefix("configuration")]
I'm presently working on a project that has been upgraded to Webapi2 from Webapi. Part of the conversion includes the switch to using attribute based routing.
I've appropriately setup my routes in the Global.asax (as follows)
GlobalConfiguration.Configure(config => config.MapHttpAttributeRoutes());
and removed the previous routing configuration.
I have decorated all of my API controllers with the appropriate System.Web.Http.RouteAttribute and System.Web.Http.RoutePrefixAttribute attributes.
If I inspect System.Web.Http.GlobalConfiguration.Configuration.Routes with the debugger I can see that all my expected routes are registered in the collection. Likewise the appropriate routes are available within the included generated Webapi Help Page documentation as expected.
Even though all appears to be setup properly a good number of my REST calls result in a 404 not found response from the server.
I've found some notable similarities specific to GET methods (this is all I've tested so far)
If a method accepts 0 parameters it will fail
If a route overrides the prefix it will fail
If a method takes a string parameter it is likely to succeed
return type seems to have no affect
Naming a route seems to have no affect
Ordering a route seems to have no affect
Renaming the underlying method seems to have no affect
Worth noting is that my API controllers appear in a separate area, but given that some routes do work I don't expect this to be the issue at hand.
Example of non-functional method call
[RoutePrefix("api/postman")]
public class PostmanApiController : ApiController
{
...
[HttpGet]
[Route("all", Name = "GetPostmanCollection")]
[ResponseType(typeof (PostmanCollectionGet))]
public IHttpActionResult GetPostmanCollection()
{
return Ok(...);
}
...
}
I expect this to be available via http://[application-root]/api/postman/all
Interestingly enough a call to
Url.Link("GetPostmanCollection", null)
will return the above expected url
A very similar example of method calls within the same controller where some work and some do not.
[RoutePrefix("api/machine")]
public class MachineApiController : ApiController
{
...
[HttpGet]
[Route("byowner/{owner}", Name = "GetPostmanCollection")]
public IEnumerable<string> GetByOwner([FromUri] string owner)
{
...
}
...
[HttpGet]
[Route("~/api/oses/{osType}")]
public IEnumerable<OsAndVersionGet> GetOSes([FromUri] string osType)
{
...
}
...
}
Where a call to http://[application-root]/api/machineby/ownername succeeds and http://[application-root]/api/oses/osType does not.
I've been poking at this far too long, any idea as to what the issue may be?
Check that you configure your HttpConfiguration via the MapHttpAttributeRoutes method before any ASP.NET MVC routing registration.
In accordance to Microsoft's CodePlex entry on Attribute Routing in MVC and Web API the Design section states:
In most cases, MapHttpAttributeRoutes or MapMvcAttributeRoutes will be
called first so that attribute routes are registered before the global
routes (and therefore get a chance to supersede global routes).
Requests to attribute routed controllers would also be filtered to
only those that originated from an attribute route.
Therefore, within the Global.asax (or where registering routes) it is appropriate to call:
GlobalConfiguration.Configure(c => c.MapHttpAttributeRoutes()); // http routes
RouteTable.Routes.MapRoute(...); // mvc routes
In my case it was a stupid mistake, I am posting this so people behind me making the same mistake may read this before they check everything else at quantum level.
My mistake was, my controller's name did not end with the word Controller.
Happy new year