WebApi PushStreamContent Error Handling - c#

What's the proper way to handle errors when using Pushstreamcontent?
I use Pushstreamcontent to stream data directly from database to a client.
On the client I use HttpCompletionOption.ResponseHeadersRead when recieving the result.
In the case the data is not available, I want to return a HttpStatusCode 404 (Not Found) for example.
Currently I only detect that there is no data, during the execution of the lambda (CopyBinaryValueToResponseStream).
At that point in time I cannot change the state of the HttpResponeMessage anymore.
So what is a proper way to handle such cases? I wanted to avoid an additional check in the database upfront, but right now that seems to be the only way to get it done?
[Route("{id}")]
public HttpResponseMessage GetImage(int id)
{
HttpResponseMessage resp = new HttpResponseMessage();
// do I need to check here first if the data is available?
// and return 404 if the data is not available
// resp.StatusCode = HttpStatusCode.NotFound
// or can I handle it later from within the lambda?
resp.Content = new PushStreamContent(async (responseStream, content, context) =>
{
// what if an error happens in this function? who do I get that error to the client?
await CopyBinaryValueToResponseStream(responseStream, id);
});
return resp;
}

You cannot fix it within the PushStreamContent action. By the time that action is executing, you have already started sending the response, and thus have already sent a 200. This is a drawback of PushStreamContent.
If you have some way to detect that the resource does not exist before streaming (for example, if some file does not exist), you can detect that first and return a 404, i.e. not using PushStreamContent at all in that case.
[Route("{id}")]
public HttpResponseMessage GetImage(int id)
{
HttpResponseMessage resp = new HttpResponseMessage();
if (File.Exists(#"c:\files\myfile.file"))
{
resp.StatusCode = HttpStatusCode.NotFound;
return resp;
}
// file exists - try to stream it
resp.Content = new PushStreamContent(async (responseStream, content, context) =>
{
// can't do anything here, already sent a 200.
await CopyBinaryValueToResponseStream(responseStream, id);
});
return resp;
}

Related

How to get status code of the response from web service call

I am trying to troubleshoot the response object I get from the web service call.
When I try response.StatusCode in ItemService.cs. Says
does not contain definition for 'Statuscode'. Are you missing directive or assembly reference.
I would appreciate if anyone could advise me on how to catch the exact response code and error message.
ItemService.cs
public async Task<List<Item>> GetItems()
{
var response = await _httpClient.GetFromJsonAsync<List<Item>>("api/item");
if(response.StatusCode)// error
{}
}
You need to use HttpClient.GetAsync method to return the value of Task<HttpResponseMessage>.
In the HttpResponseMessage class, it contains StatusCode property which is what you need.
Updated:
To check whether the response returns a success code, you should use the IsSuccessStatusCode property. Otherwise, you should compare the status code below:
if (response.StatusCode != HttpStatusCode.OK)
Next, extract the content from the HttpResponseMessage with HttpContentJsonExtensions.ReadFromJsonAsync method.
public async Task<List<Item>> GetItems()
{
var response = await _httpClient.GetAsync("api/Item");
if (!response.IsSuccessStatusCode)
{
Console.WriteLine(response.StatusCode);
return new List<Item>();
}
return await response.Content.ReadFromJsonAsync<List<Item>>();
}
Reference
Make HTTP requests using IHttpClientFactory in ASP.NET Core (CreateClient section with demo)

ResponseStream from API results in empty values on deserializing

Returning data from my API and deserializing it in the client controller, the content seems to empty. I've verified that the API is returning valid data, and it the "count" in the response is accurate. However, the data when serialized is seems not be initialized.
Not quite sure where to look for what's causing the issue. My best theory is that the data is being deserialized before it's materialized due to the async, but that doesn't explain why the count is correct (2 records).
public async Task<IActionResult> Index()
{
var httpClient = _httpClientFactory.CreateClient("APIClient");
var request = new HttpRequestMessage(HttpMethod.Get, "/api/AppUsers");
var response = await httpClient.SendAsync(request).ConfigureAwait(false);
if (response.IsSuccessStatusCode)
{
using (var responseStream = await response.Content.ReadAsStreamAsync())
{
var users = await JsonSerializer.DeserializeAsync<IList<AppUser>>(responseStream);
return View(new IndexViewModel(users));
}
}
else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized ||
response.StatusCode == System.Net.HttpStatusCode.Forbidden)
{
return RedirectToAction("AccessDenied", "Authorization");
}
throw new Exception("Problem accessing the API");
}
API Method which is called
[HttpGet]
public IActionResult GetAppUsers()
{
var users = _appUserService.ListAppUsers();
return Ok(users);
}
Calling the API Method directly shows the 2 rows returned as expected
After some debugging, I noticed that the json structure returned from the API is having first letter in property name as lowercase, but the AppUser class have first letter UpperCase. Not sure why this happens, but that's the reason why the deserialization fails.
Try to deserialize to List instead to IList:
var users = await JsonSerializer.DeserializeAsync<List<AppUser>>(responseStream);
I think that you should remove ConfigureAwait(false) so you can remain in the same synchronizaton context.
For more info check this link: https://devblogs.microsoft.com/dotnet/configureawait-faq/
After noticing the properties changing from upper case to lower case on the JSON object, the solution will be to either force JSON to PascalCase as the default now is camelCase
https://codeopinion.com/asp-net-core-mvc-json-output-camelcase-pascalcase/
or I need to consider the camelCase on deserialization.
This issue stole several hours from my life :/
Hope this post can save some hours for somebody else.
One solution would be to disable the defaulting
builder.Services.AddControllers(options =>
{
options.ReturnHttpNotAcceptable = true;
})
.AddXmlSerializerFormatters()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.PropertyNamingPolicy = null; //to avoid defaulting to camelCase
});

Returning custom HttpResponseMessage as IActionResult

I have a web api which returns IActionResult.
I return FileContentResult from this api like this
return new FileContentResult(model.Content, ContentType)
{
EnableRangeProcessing = true
};
I have a requirement in which I now want to control StatusCode myself, rather than FileContentResult decide itself.
I don't find any way to do this.
Basically I want to return my own designed HttpResponseMessage in which I can set headers and other stuff myself.
But I don't find any way to do this for IActionResult type.
The only thing that I thought could work is to use ResponseMessageResult something like this
var content = new ByteArrayContent(bytesWithValidationData);
var response = new HttpResponseMessage();
response.Content = content;
response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
response.StatusCode = HttpStatusCode.PartialContent;
response.Content.Headers.ContentRange = new ContentRangeHeaderValue(from, to);
return new ResponseMessageResult(response);
But its response is not same as HttpResponse, it just returns json result with HttpResponseMessage object details but does not actually return Http response considering content type etc. where I can download the file.
It gives result like this
Is there any way I can return my designed file result type http response?
Legacy ASP.NET Core web API had special handling for raw HttpResponseMessage instances. ASP.NET Core does not - your controller action has to return an instance of IActionResult.
In your case, I would suggest subclassing FileContentResult and manipulating the status code, then returning your subclass from your controller. Something like the following:
public class MyFileContentResult : FileContentResult
{
public override Task ExecuteResultAsync(ActionContext context)
{
context.HttpContext.Response.StatusCode = <your status code>;
var result = base.ExecuteResultAsync(context);
return result;
}
}

Web Api 2 returning data without formatting it async

After fetching data from the database, I serialize it into XML.
I then write that XML into a Redis caching instance as a string.
I want the endpoint to check if data exists in the cache and based on the result either return the data from the cache, or hit the DB, cache the data and then return that.
My code works just fine when executed synchronously:
Working Synchronous Code
[HttpGet]
public IHttpActionResult Test(int userId)
{
var response = new HttpResponseMessage(HttpStatusCode.OK);
var data = Cache.StringGet("Cart:" + userId);
if (string.IsNullOrEmpty(data))
{
// Grab data from DB and format as XML
Cache.StringSet("Cart:" + userId, data, TimeSpan.FromHours(2));
}
response.Content = new StringContent(data);
response.Content.Headers.ContentType = new MediaTypeHeaderValue("text/xml");
return new ResponseMessageResult(response);
}
Where everything goes bananas is when trying to make it Asynchronous.
Broken Async Code
( I included the smallest amount of code necessary to reproduce the issue )
[HttpGet]
public async Task<HttpResponseMessage> TestAsync(int userId)
{
var data = await Cache.StringGetAsync("Cart:" + userId);
var response = Request.CreateResponse(HttpStatusCode.OK);
response.Content = new StringContent("<test>Test</test>");
response.Content.Headers.ContentType = new MediaTypeHeaderValue("text/xml");
return response;
}
Note that in this example above I'm not even accessing the asynchronously loaded data. If I comment out the await line, things start working again. It only fails if a await is in the code.
The problem that occurs is that 50% of the time, requests to the endpoint just ... stall and never finish. Fiddler screenshot attached to highlight the issue.
Finally, if there is a easier way to skip media formatting and content negotiation, i'd be more than happy to change my approach.
I should add that the service that will consume this endpoint only supports XML, and it made no sense to me to deserialize and reserialize on every request.
Problem Solved!
It ended up being Azure Application Insights.
I guess it does not fully support async or has issues with async in combition with manually creating HttpResponseMessages.
Thank you all for the responses.

The request message was already sent. Cannot send the same request message multiple times

Is there anything wrong with my code here? I keep getting this error:
System.InvalidOperationException: The request message was already sent. Cannot send the same request message multiple times.
My HttpRequestMessage is inside a Func so I figured I get a brand new request each time I pass in func().
public async Task<HttpResponseMessage> GetAsync(HttpRequestMessage request)
{
return await RequestAsync(() => request);
}
public async Task<HttpResponseMessage> RequestAsync(Func<HttpRequestMessage> func)
{
var response = await ProcessRequestAsync(func);
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
WaitForSomeTime();
response = await ProcessRequestAsync(func);
}
return response;
}
private async Task<HttpResponseMessage> ProcessRequestAsync(Func<HttpRequestMessage> func)
{
var client = new HttpClient();
var response = await client.SendAsync(func()).ConfigureAwait(false);
return response;
}
You are calling the same func parameter twice:
var response = await ProcessRequestAsync(func);
//...
response = await ProcessRequestAsync(func);
In this case func returns the same request every single time. It doesn't generate a new one every time you call it. If you truly need a different request each time then func needs to return a new message each call:
var response = await GetAsync(() => new HttpRequestMessage()); // Create a real request.
public async Task<HttpResponseMessage> GetAsync(Func<HttpRequestMessage> requestGenerator)
{
return await RequestAsync(() => requestGenerator());
}
I had the same issue, but no repetition in my code. Turned out I had added a watch on an asynchronous process. That watch called the process while I stepped through the code, so that when I got to the line I was trying to debug it crashed with this error message.
Removing all watches solved the problem.
Leaving this here for other people who might have the same problem.

Categories