I'm trying to improve the logging of an un-successful HttpResponseMessage responses after sending HttpClient.SendAsync requests.
Using the System.Net.Http.IHttpClientFactory feature of .net-core this code makes request:
POST https//fooApp.fooDomain.com/fooMethod
Content-Type: application/json; charset=utf-8
startup.cs:
services.AddHttpClient("MyHttpClient", client =>
{
client.BaseAddress = new Uri("https//fooApp.fooDomain.com/");
client.Timeout = new TimeSpan(0, 0, 15);
});
MyHttpService.cs:
public class MyHttpService
{
private readonly IHttpClientFactory _factory;
public MyHttpService(IHttpClientFactory factory)
{
_factory = factory;
}
private async Task<HttpResponseMessage> GetHttpResponseMessage(...)
{
try
{
HttpClient httpClient = this._factory.CreateClient("MyHttpClient");
using (request.HttpContent)
{
var requestMessage = new HttpRequestMessage
{
RequestUri = new Uri("fooMethod", UriKind.Relative),
Method = HttpMethod.Post,
Content = new StringContent(string.Empty, Encoding.UTF8, "application/json")
};
using (requestMessage)
{
return await httpClient.SendAsync(requestMessage);
}
}
}
catch (Exception ex)
{
//Log the exception...
return null;
}
}
}
The problem is when SendAsync throws exception (like TaskCanceledException or OperationCanceledException due to timeout) I would like to look at IsCancellationRequested of the cancellation token and log a proper message if true.
My question is how (and where) is it best to create the CancellationToken to be passed into the SendAsync method? in order to catch it when timeouts occurs. for my understanding the token must be coordinated with the HttpClient timeout's timespan that has been initialized at startup.
try
{
//...
return await httpClient.SendAsync(requestMessage, cts);
}
catch (TaskCanceledException ex)
{
if (cts.IsCancellationRequested) //log time-out
else //log general
}
catch (Exception ex)
{
//log general
}
Related
I am developing WebAPI and want to catch all my ApiException custom exceptions and display WebAPI-friendly responses. The ApiException exception can be thrown from Action or Filter like IAuthorizationFilter or ActionFilterAttribute.
First I tried to use IExceptionFilter but later I found that the IExceptionFilter handles only exceptions thrown from Actions and not from other Filters.
public class ApiExceptionFilter : IExceptionFilter
{
public void OnException(ExceptionContext context)
{
var exception = context.Exception;
if (exception is not ApiException responseException)
{
responseException = new ApiException(ResponseMessageType.UnhandledException);
}
context.Result = new ObjectResult(new ResultMessageDto(responseException))
{
StatusCode = responseException.HttpStatusCode
};
}
}
The second approach that I found many suggest to use is the Middleware but this is not the correct way by WebAPI design.
public class ErrorHandlerMiddleware
{
private readonly RequestDelegate _next;
public ErrorHandlerMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception exception)
{
var response = context.Response;
response.ContentType = "application/json";
if (exception is not ApiException responseException)
{
responseException = new ApiException(ResponseMessageType.UnhandledException);
}
response.StatusCode = responseException.HttpStatusCode;
await response.WriteAsJsonAsync(new ResultMessageDto(responseException), new JsonSerializerOptions()
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
PropertyNamingPolicy = null
});
}
}
}
The Middleware exception handling skips WebAPI MVC OutputFormaters and responds only in JSON or what is set by the developer. This solution is bad by design because do not respect Accept header.
How to handle Exceptions in Actions and Filters without leaving MVC scope?
I feel it is best to handle in Middleware, Here is a sample code. I am using Json but over here, you can use any output formatter (even custom ones).
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
try
{
await next(context);
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex);
}
}
private async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
int statusCode = StatusCodes.Status500InternalServerError;
string exceptionType, exceptionDetail;
context.Response.ContentType = "application/json";
switch (exception)
{
case ArgumentNullException:
exceptionType = "Argument Null Exception";
exceptionDetail = exception.Message;
break;
case ApiException:
exceptionType = "API Exception";
exceptionDetail = exception.Message;
break;
default:
exceptionType = "Unhandled"
exceptionDetail = "Something went wrong";
break;
}
var problemDetails = new Microsoft.AspNetCore.Mvc.ProblemDetails
{
Status = statusCode,
Type = exceptionType,
Title = "An error has occurred within the PromComm Application",
Detail = exceptionDetail,
Instance = context.Request.Path
};
// You can use any formatter here even with custom messages
var problemDetailsJson = System.Text.Json.JsonSerializer.Serialize(problemDetails);
context.Response.StatusCode = statusCode;
await context.Response.WriteAsync(problemDetailsJson);
}
C# 10/.NET 6
I am struggling to identify what is causing this error. From this code:
internal class Call
{
internal int RetryCount { get; set; } = 1;
internal HttpClient Client { get; init; }
internal HttpRequestMessage Request { get; private set; }
internal Call(HttpClient client, HttpRequestMessage request)
{
...
}
internal async Task<T> SendRequestAsync<T>()
{
JsonSerializerOptions serializerOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
while (true)
{
if (this.RetryCount > Setup.Config.RetryMax)
throw new TimeoutException($"Maximum number of retries reached.");
await LogRequest(this.Client, this.Request);
Thread.Sleep(Setup.Config.ThrottleBuffer);
try
{
HttpResponseMessage? response = new();
response = await this.Client.SendAsync(this.Request);
await LogResponse(response);
_ = response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<T>(serializerOptions) ?? throw new Exception("Result object was null after JSON deserialization.");
}
catch (HttpRequestException ex)
{
if (ex.StatusCode is HttpStatusCode.TooManyRequests or HttpStatusCode.InsufficientStorage or HttpStatusCode.ServiceUnavailable)
{
Log.Error($"HTTP request failed. Retrying in {(Setup.Config.RetryDelay / 1000) * this.RetryCount} seconds...");
Thread.Sleep(Setup.Config.RetryDelay * this.RetryCount);
this.RetryCount++;
continue;
}
else if (ex.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden)
{
Log.Error($"HTTP request failed. Permissions error: unable to retry.");
throw;
}
else
{
throw;
}
}
}
}
}
When called repeatedly, it runs 15 times and then errors on the 16th run. I get this error:
Object name: System.Net.Http.HttpClient.
at System.Net.Http.HttpClient.CheckDisposed()
at System.Net.Http.HttpClient.CheckRequestBeforeSend(HttpRequestMessage request)
at System.Net.Http.HttpClient.SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationToken cancellationToken)
at System.Net.Http.HttpClient.SendAsync(HttpRequestMessage request)
at [redacted].Call.SendRequestAsync[T]() in C:\Users\[redacted]\Call.cs:line 97
Line 97 points to this line: response = await this.Client.SendAsync(this.Request)
I found this thread where a user had a similar problem with StringContent being automatically disposed by HttpClient. However, I have HttpResponseMessage being created each time in the loop.
What am I missing?
The error was pointing to the HttpClient being disposed, not HttpResponseMessage. The consumer was disposing the client prematurely.
I am trying to create a middleware that can log the response body as well as manage exception globally and I was succeeded about that. My problem is that the custom message that I put on exception it's not showing on the response.
Middleware Code 01:
public async Task Invoke(HttpContext context)
{
context.Request.EnableRewind();
var originalBodyStream = context.Response.Body;
using (var responseBody = new MemoryStream())
{
try
{
context.Response.Body = responseBody;
await next(context);
context.Response.Body.Seek(0, SeekOrigin.Begin);
var response = await new StreamReader(context.Response.Body).ReadToEndAsync();
context.Response.Body.Seek(0, SeekOrigin.Begin);
// Process log
var log = new LogMetadata();
log.RequestMethod = context.Request.Method;
log.RequestUri = context.Request.Path.ToString();
log.ResponseStatusCode = context.Response.StatusCode;
log.ResponseTimestamp = DateTime.Now;
log.ResponseContentType = context.Response.ContentType;
log.ResponseContent = response;
// Keep Log to text file
CustomLogger.WriteLog(log);
await responseBody.CopyToAsync(originalBodyStream);
}
catch (Exception ex)
{
context.Response.ContentType = "application/json";
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
var jsonObject = JsonConvert.SerializeObject(My Custom Model);
await context.Response.WriteAsync(jsonObject, Encoding.UTF8);
return;
}
}
}
If I write my middleware like that, my custom exception is working fine but I unable to log my response body.
Middleware Code 02:
public async Task Invoke(HttpContext context)
{
context.Request.EnableRewind();
try
{
await next(context);
}
catch (Exception ex)
{
context.Response.ContentType = "application/json";
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
var jsonObject = JsonConvert.SerializeObject(My Custom Model);
await context.Response.WriteAsync(jsonObject, Encoding.UTF8);
return;
}
}
My Controller Action :
[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
throw new Exception("Exception Message");
}
Now I want to show my exception message with my middleware 01, but it doesn't work but its work on my middleware 02.
So my observation is the problem is occurring for reading the context response. Is there anything I have missed in my middleware 01 code?
Is there any better way to serve my purpose that log the response body as well as manage exception globally?
I think what you are saying is that this code isn't sending it's response to the client.
catch (Exception ex)
{
context.Response.ContentType = "application/json";
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
var jsonObject = JsonConvert.SerializeObject(My Custom Model);
await context.Response.WriteAsync(jsonObject, Encoding.UTF8);
return;
}
The reason for this is that await context.Response.WriteAsync(jsonObject, Encoding.UTF8); isn't writing to the original body stream it's writing to the memory stream that is seekable. So after you write to it you have to copy it to the original stream. So I believe the code should look like this:
catch (Exception ex)
{
context.Response.ContentType = "application/json";
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
var jsonObject = JsonConvert.SerializeObject(My Custom Model);
await context.Response.WriteAsync(jsonObject, Encoding.UTF8);
context.Response.Body.Seek(0, SeekOrigin.Begin); //IMPORTANT!
await responseBody.CopyToAsync(originalBodyStream); //IMPORTANT!
return;
}
There is a wonderful article explaining in detail your problem - Using Middleware to trap Exceptions in Asp.Net Core.
What you need to remember about middleware is the following:
Middleware is added to your app during Startup, as you saw above. The order in which you call the Use... methods does matter! Middleware is "waterfalled" down through until either all have been executed, or one stops execution.
The first things passed to your middleware is a request delegate. This is a delegate that takes the current HttpContext object and executes it. Your middleware saves this off upon creation, and uses it in the Invoke() step.
Invoke() is where the work is done. Whatever you want to do to the request/response as part of your middleware is done here. Some other usages for middleware might be to authorize a request based on a header or inject a header in to the request or response
So what you do, you write a new exception type, and a middleware handler to trap your exception:
New Exception type class:
public class HttpStatusCodeException : Exception
{
public int StatusCode { get; set; }
public string ContentType { get; set; } = #"text/plain";
public HttpStatusCodeException(int statusCode)
{
this.StatusCode = statusCode;
}
public HttpStatusCodeException(int statusCode, string message) : base(message)
{
this.StatusCode = statusCode;
}
public HttpStatusCodeException(int statusCode, Exception inner) : this(statusCode, inner.ToString()) { }
public HttpStatusCodeException(int statusCode, JObject errorObject) : this(statusCode, errorObject.ToString())
{
this.ContentType = #"application/json";
}
}
And the middlware handler:
public class HttpStatusCodeExceptionMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<HttpStatusCodeExceptionMiddleware> _logger;
public HttpStatusCodeExceptionMiddleware(RequestDelegate next, ILoggerFactory loggerFactory)
{
_next = next ?? throw new ArgumentNullException(nameof(next));
_logger = loggerFactory?.CreateLogger<HttpStatusCodeExceptionMiddleware>() ?? throw new ArgumentNullException(nameof(loggerFactory));
}
public async Task Invoke(HttpContext context)
{
try
{
await _next(context);
}
catch (HttpStatusCodeException ex)
{
if (context.Response.HasStarted)
{
_logger.LogWarning("The response has already started, the http status code middleware will not be executed.");
throw;
}
context.Response.Clear();
context.Response.StatusCode = ex.StatusCode;
context.Response.ContentType = ex.ContentType;
await context.Response.WriteAsync(ex.Message);
return;
}
}
}
// Extension method used to add the middleware to the HTTP request pipeline.
public static class HttpStatusCodeExceptionMiddlewareExtensions
{
public static IApplicationBuilder UseHttpStatusCodeExceptionMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<HttpStatusCodeExceptionMiddleware>();
}
}
Then use your new middleware:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseHttpStatusCodeExceptionMiddleware();
}
else
{
app.UseHttpStatusCodeExceptionMiddleware();
app.UseExceptionHandler();
}
app.UseStaticFiles();
app.UseMvc();
}
The end use is simple:
throw new HttpStatusCodeException(StatusCodes.Status400BadRequest, #"You sent bad stuff");
I have written a very simple WebApiClient extending HttpClient. The code is following. The main reason to do that was to throw MyOwnWebApiException when httpResponse.IsSuccessStatusCode is false.
public class WebApiClient : HttpClient
{
public WebApiClient(string apiBaseUrl)
{
this.BaseAddress = new Uri(apiBaseUrl);
this.DefaultRequestHeaders.Accept.Clear();
}
public void AddAcceptHeaders(MediaTypeWithQualityHeaderValue header)
{
this.DefaultRequestHeaders.Accept.Add(header);
}
public async Task<string> DoPost(string endPoint, Object dataToPost)
{
HttpResponseMessage httpResponse = await ((HttpClient)this).PostAsJsonAsync(endPoint, dataToPost);
if (httpResponse.IsSuccessStatusCode)
{
string rawResponse = await httpResponse.Content.ReadAsStringAsync();
return rawResponse;
}
else
{
string rawException = await httpResponse.Content.ReadAsStringAsync();
MyOwnWebApiErrorResponse exception =
JsonConvert.DeserializeObject<MyOwnApiErrorResponse>(rawException, GetJsonSerializerSettings());
throw new MyOwnWebApiException (exception.StatusCode,exception.Message,exception.DeveloperMessage,exception.HelpLink);
}
}
#region "Private Methods"
private static JsonSerializerSettings GetJsonSerializerSettings()
{
// Serializer Settings
var settings = new JsonSerializerSettings()
{
TypeNameHandling = TypeNameHandling.All,
ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor,
ObjectCreationHandling = ObjectCreationHandling.Auto
};
return settings;
}
#endregion
Following is the code of the class using WebApiClient.
class TestWebApiClient
{
private WebApiClient _client;
public ComputationProcessesWebApiClient()
{
_client = new WebApiClient("http://test.api/");
_client.AddAcceptHeaders(new MediaTypeWithQualityHeaderValue("application/json"));
}
public void GetData(string dataFor)
{
try
{
DataRequest request = new DataRequest();
request.dataFor = dataFor;
**// THIS LINE IS THROWING AGGREGATEEXCEPTION--- **I WANT MyOwnException ****
string response = _client.DoPost("GetData", request).Result; // Use the End Point here ....
}
catch (MyOwnWebApiException exception)
{
//Handle exception here
}
}
}
Question
In the TestWebApiClient class, i dont want to catch AggregateException, rather i want to keep it more elegent and catch MyOwnWebApiException, but the problem is the line ** _client.DoPost("GetData", request).Result** throws an AggregateException if something goes wrong from the WebApi. How to change the code so that from TestWebApiClient i only have to catch MyOwnException ??
This is as a result of synchronously waiting for your task. If you stay async and await your task instead, you'll find that your actual Exception is the one that is caught.
Compare the following below:
void Main()
{
TryCatch();
TryCatchAsync();
}
void TryCatch()
{
try
{
ThrowAnError().Wait();
}
catch(Exception ex)
{
//AggregateException
Console.WriteLine(ex);
}
}
async Task TryCatchAsync()
{
try
{
await ThrowAnError();
}
catch(Exception ex)
{
//MyException
Console.WriteLine(ex);
}
}
async Task ThrowAnError()
{
await Task.Yield();
throw new MyException();
}
public class MyException:Exception{};
Top hint for async/await? It's async/await all the way down. The moment you .Wait() or .Result on a Task, things start to get messy.
I'm trying to build an MVC that requests through a PCL to a WebApi. I am sending a get requests and getting stuck on the awaiting for the response. Postman returns the correct values. I also don t get exceptions on send. The 3 projects are both on the same solution.
PCL
HttpResponseMessage httpResponse = null;
try
{
httpResponse = await _http.GetAsync( "http://localhost:43818/api/values" );
}
catch (Exception e)
{
var meessage = e.Message;
var stack = e.StackTrace;
}
if (httpResponse.StatusCode == HttpStatusCode.OK)
{
string json = await httpResponse.Content.ReadAsStringAsync( );
}
So the issue is that in the PCL, it's doesn pass the await, it gets stuck.
MVC
var result = apiClient.GetIndex( );
Web Api
public class ValuesController : ApiController
{
// GET api/values
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
}
Also, how do i wait in my MVC for the response before rendering the controller view
In your Class library (PCL), Create method GetIndex as below,
public async Task GetIndexAsync()
{
HttpResponseMessage httpResponse = null;
try
{
_http.BaseAddress = new Uri("http://localhost:43818/");
httpResponse = await _http.GetAsync("api/values");
}
catch (Exception e)
{
var meessage = e.Message;
var stack = e.StackTrace;
}
if (httpResponse.StatusCode == HttpStatusCode.OK)
{
string json = await httpResponse.Content.ReadAsStringAsync();
}
}
And In MVC calling method as below,
var result = apiClient.GetIndexAsync().Wait();
which solved both your problems.
Ok so i found the best sollution. Blocking threads is not a very good idea.
This is the fix
PCL
public async Task<HttpResponseMessage> Register()
{
HttpRequestMessage request = new HttpRequestMessage
{
RequestUri = new Uri( _http.BaseAddress, "account/register/" ),
Method = HttpMethod.Post,
Content = new StringContent( "{\"Email\": \"email#yahoo.com\",\"Password\": \"Password!1\",\"ConfirmPassword\": \"Password!1\"}",
Encoding.UTF8,
_contentType
),
};
HttpResponseMessage response = new HttpResponseMessage();
try
{
response = await _http.SendAsync( request, CancellationToken.None );
}
catch (Exception e)
{
Debugger.Break();
}
return response;
}
MVC Client
public async Task<ViewResult> Index( )
{
var thisTask = await Api.Register( );
return View();
}