Web API 2 Post 404s, but Get works - asp.net-web-api2

I'm confused... I have a very simple Web API and controller, which works fine if I have a GET request, but 404's if I have a POST request.
[RoutePrefix("api/telemetry/trial")]
public class LoginTelemetryController : ApiController
{
[Route("login")]
[HttpPost]
public IHttpActionResult RecordLogin(string appKey) {
using (var context = new Core.Data.CoreContext()) {
context.ActivityLogItems.Add(new Domain.Logging.ActivityLogItem()
{
ActivityType = "Trial.Login",
DateUtc = DateTime.UtcNow,
Key = new Guid(appKey)
});
context.SaveChanges();
}
return Ok();
}
When I post against this in postman, I get:
{
"message": "No HTTP resource was found that matches the request URI 'http://localhost:47275/api/telemetry/trial/login'.",
"messageDetail": "No action was found on the controller 'LoginTelemetry' that matches the request."
}
If I change it to a [HttpGet] and put the appKey as a querystring, all is fine.
My app startup is very simple:
public void Configuration(IAppBuilder app)
{
log4net.Config.XmlConfigurator.Configure();
HttpConfiguration httpConfig = new HttpConfiguration();
httpConfig.MapHttpAttributeRoutes(); // <------ HERE
FilterConfig.RegisterHttpFilters(httpConfig.Filters);
LoggingConfig.RegisterHandlers(httpConfig.Services);
ConfigureOAuth(app);
ConfigureWebApi(httpConfig);
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
app.UseWebApi(httpConfig);
}
Can anyone spot why POST requests aren't being found? Thanks

If I take string parameter out and replace it with a request object, it works...
Instead of: public IHttpActionResult RecordLogin(string appKey)
I create a request model class:
public class PostLoginTelemetryRequest{
public string appKey {get;set;}
}
Then alter the signature:
public IHttpActionResult RecordLogin(PostLoginTelemetryRequest request)
Everything works fine (why it can't take a regular string like MVC5 web dev, I don't know, but anyway...)
(also note that I had tried this in every format from the client with the string method: form-url-encode, raw body, etc, so I'm fairly certain it wasn't a calling format issue).

Related

Blazor WASM Http call is not hitting some API endpoints. Receiving index.html instead

I have a Blazor WASM page that need to make a call to get some data from an API. The Blazor app is ASPNetCore hosted, and the hosting app contains the API.
Some of my endpoints work, but some calls throw a Json serialization exception.
Unhandled exception rendering component: '<' is an invalid start of a value. Path: $ | LineNumber: 0 | BytePositionInLine: 0.
If I look at the actual response from the server, it looks like it returns the content of index.html from my WASM app.
Example Controller
[Authorize]
[ApiController]
[Route("api/[controller]")]
public class CompanyController : ControllerBase
{
private readonly ApplicationDbContext _context;
public CompanyController(ApplicationDbContext context)
{
_context = context;
}
[HttpGet("{id}")]
public async Task<IActionResult> Get(long id)
{
Company? company = await _context.Companies.FindAsync(id);
if (company == null)
{
return NotFound();
}
return Ok(company);
}
}
Example Blazor Page
#page "/companies/{id:long}"
#attribute [Authorize]
#inject HttpClient Http
#inject NavigationManager Nav
#if (company != null)
{
<div>#company.Name</div>
}
else
{
<div>Loading Company...</div>
}
#code {
private Company? company;
[Parameter]
public long Id { get; set; }
protected override async Task OnInitializedAsync()
{
try
{
company = await Http.GetFromJsonAsync<Company>($"/api/company/{Id}");
}
catch (AccessTokenNotAvailableException exception)
{
exception.Redirect();
}
}
}
In the example above, everything works as expected. But if I make the following two changes, I'll get the Json Exception mentioned above.
Create an identical controller named WorkOrderController. Everything else is identical including pulling the Company data from the database. Only the name of the controller is different.
Change the Http request to company = await Http.GetFromJsonAsync<Company>($"/api/workOrder/{Id}"); in the Blazor page.
Why would some endpoints work, and some wouldn't?
So, the requestUri passed to GetFromJsonAsync must be lowercase. My request was failing because I had a capital "O" in "workOrder".
I am not sure why this is a requirement of the request parameter, but alas, making the path lowercase fixed the issue.

415 Unsupported Media Type in ASP.NET core web api

I am trying to experiment with asp.net core web api so I made some simple api with a controller like this:
[ApiController]
[Route("MyController")]
public class MyController : ControllerBase
{
[HttpGet]
[Route("GetResult")]
public IActionResult GetResult(string param1, string param2= null, SomeClassObj obj = null)
{ .... }
}
I ran the api locally and sent this postman GET request:
https://localhost:5001/MyController/GetResult?param1=someString
I got the error: 415 Unsupported Media Type
What am I missing here so it could work?
I was getting the same error after invoking the WEB API from .NET MVC.
As suggested by #zhulien, I have changed from [FromBody] to [FromForm] in WebAPI, it works fine for me.
.NET Core WebAPI method.
public async Task<IActionResult> Login([FromForm] LoginModel loginInfo)
{ // JWT code here }
.Net Core MVC Action Method.
public async void InvokeLoginAPIAsync(string endPoint, string userName, string pwd)
{
configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.Build();
baseUrl = configuration["Application:BaseAPI"] ?? throw new Exception("Unable to get the configuration with key Application:BaseAPI");
string targetUrl = string.Format("{0}/{1}", baseUrl, endPoint);
using (HttpClient deviceClient = new HttpClient())
{
var request = new HttpRequestMessage(HttpMethod.Post, targetUrl);
var data = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("userName", userName),
new KeyValuePair<string, string>("password", pwd)
};
request.Content = new FormUrlEncodedContent(data);
using (var response = await deviceClient.SendAsync(request))
{
if (response.StatusCode == HttpStatusCode.OK)
{
TempData["Response"] = JsonConvert.SerializeObject(response.Content);
}
}
}
}
Which version of .NET Core are you using?
Try doing the request from the browser and see if you have the same result.
Also, are you sure you're doing a GET and not a POST request in Postman? You shouldn't get 415 errors for GET requests, especially when you're not sending any body.
This error mainly occurs when you try to send a body and you haven't specified the media-type through the Content-Type header.
Ensure that the request is GET and your body is empty.
Solution after post edit:
As you're trying to parse a DTO object(SomeClassObj), you should specify where the values should come from. In order to fix your specific case, add the [FromQuery] attribute before SomeClassObj.
Your code should look like this:
[ApiController]
[Route("MyController")]
public class MyController : ControllerBase
{
[HttpGet]
[Route("GetResult")]
public IActionResult GetResult(string param1, string param2= null, [FromQuery]SomeClassObj obj = null)
{ .... }
}
This tells the parser to fetch the data from the query string. This will fix the 415 issue. However, if you want to bind to complex types, especially on get, checkout those topics: ASP.NET CORE 3.1 Model Binding and this issue as you will most probably encounter issues with parsing your DTO object.
Use [FromForm] attribute before each argument in the controller function.

Unable to configure route for Get in WebApi 2

I'm struggling with something very basic. I'm trying to be get a response from my WebApi2 restful service, and I can't.
I have not edited the default WebApi (WebApiConfig.cs) route.
This is the controller
public class AboutController
{
[Route("api/about/{id:int}/{service1}/{service2}")]
public async Task<IHttpActionResult> Get(int accountId, string mainservice, string secondaryservice)
{
//logic
}
}
If I navigate (in a browser) to http://localhost:58090/api/about I get the error message The requested resource does not support http method 'GET'. I guess this makes sense, as it doesn't match the route (path).
If I update the path to something which matches the signature, such as http://localhost:58090/api/about/1/a/b I get the error message No action was found on the controller About' that matches the request.
Even if I add [HttpGet] to the controller, it makes no difference.
As a sanity test, I updated to
public class AboutController
{
public async Task<IHttpActionResult> Get()
{
//logic
}
}
and it does what is expected. I'm lost as to why adding the parameters has confused things so much.
I'm lost as to what I've done wrong
The route must match the parameters
[Route("api/about/{id:int}/{service1}/{service2}")]
public async Task<IHttpActionResult> Get(int id, string mainService, string secondaryService)
{
The above won't work, because it is expecting to see service1 and service2 based upon the route.
Update as per the example below
[Route("api/about/{id:int}/{service1}/{service2}")]
public async Task<IHttpActionResult> Get(int id, string service1, string service2)
{

Asp.Net Core 2.1 - Authorize based on content in request

I am exposing an endpoint for integration with a 3rd party and their requirement is for me to authorize their requests to my endpoint based on a key passed in the body being posted. My code will then needs to validate that the passed key matches some predetermined value on my side. The incoming model will look something like this:
public class RequestBase
{
public string ApiKey { get; set; }
...
}
Exploring the options for Authorization in ASP.NET Core I don't really see a match for what I am attempting to do. I am thinking a custom AuthorizeAttribute from this question would work but I'm not having any luck and get a 401 regardless of what I do. This is what I have so far:
[AttributeUsage(AttributeTargets.Class)]
public class MyAuthorizeAttribute : AuthorizeAttribute, IAuthorizationFilter
{
private static IEnumerable<string> _apiKeys = new List<string>
{
"some key... eventually will be dynamic"
};
public void OnAuthorization(AuthorizationFilterContext context)
{
var req = context.HttpContext.Request;
req.EnableRewind();
using (var reader = new StreamReader(req.Body, Encoding.UTF8, true, 1024, true))
{
var bodyStr = reader.ReadToEnd();
var isAuthorized = _apiKeys.Any(apiKey => bodyStr.Contains(apiKey));
if (!isAuthorized)
{
context.Result = new StatusCodeResult((int)System.Net.HttpStatusCode.Forbidden);
return;
}
}
req.Body.Position = 0;
}
}
When the key is not found in the body the 403 is returned as expected. However, when the key is found the result I get back is still a 401. Almost seems as if the base.OnAuthorization is being called. I have other endpoints that use a standard AurhorizeAttribute. They work as expected when only if I pass in a JWT.
Questions:
Am I on the right path with a custom AuthorizeAttribute or is there a better way?
If a customer AuthorizeAttribute is the right path... what am I missing?
Appreciate any help!
For using your own authorize logic with IAuthorizationFilter, you should not use with AuthorizeAttribute which will check the Authentication with default authentication schema.
Try to change AuthorizeAttribute to Attribute.
[AttributeUsage(AttributeTargets.Class)]
public class KeyAuthorizeAttribute : Attribute, IAuthorizationFilter
{

Cannot post JSON to an ASP.NET Core RazorPage handler

I'm working with an ASP.NET Core RazorPage as an alternative to an MVC controller, and I want to be able to submit the client side form via XMLHttpRequest. I've already figured out the XSRF token bits so that passes the muster, but the RazorPages framework doesn't seem to process the inbound JSON payload and bind it to the property as expected.
Some code:
The page's model (.cshtml.cs):
public class IndexModel : PageModel
{
private Database database;
private ILogger logger;
[BindProperty]
public AddRequestModel MyRequest { get; set; }
public IndexModel(Database database, ILogger<IndexModel> logger)
{
this.database = database;
this.logger = logger;
}
public void OnGet() {}
public IActionResult OnPostValidate()
{
if (ModelState.IsValid)
{
// ...
}
return new BadRequestObjectResult(ModelState);
}
public async Task<IActionResult> OnPutConfirmAsync()
{
// ...
}
}
And the client side post:
const url = "?handler=validate";
const data = { MyRequest: this.fields };
await axios.post(url, data);
I have verified the data is being submitted correctly:
That X-XSRF-TOKEN header is being added by axios before the request is submitted. The fact that the server responds with a list of errors indicates that it's not the XSRF token causing the problem:
Note the MyRequest object does not contain the values from the request payload - it was not bound as expected (FirstName would not return a required error otherwise). How can I tell RazorPages to accept the JSON request and bind it to my property?
I was able to get the Binding works by adding FromBody similar to how it worked for ASP.NET Web API 2.
[BindProperty, FromBody]
public BroadcastMessageEditingViewModel BindingInfo { get; set; }
Use urlsearchparams with formdata.
In this post you can find more information How do I post form data with fetch api?
You would be better off posting your data to an API endpoint instead of a page controller. Create a class from ControllerBase and it will handle your JSON post correctly.