Search code examples
c#asp.net.netasp.net-web-api

How do I get the real error message in HttpClient response.ReasonPhrase?


My Setting

I have 2 WebApi projects with the following flow:

  1. User makes request to API 1
  2. API 1 makes request to API 2 on behalf of the user (using an HttpClient).

using (var client = new HttpClient())
{
    client.SetBearerToken(token);

    string endpoint = PbbSettings.Identity.Users.Delete.Replace("{userId}", userId);

    // Attempt deletion of the user
    using (var response = await client.DeleteAsync(endpoint))
    {
        // Throw exception if not succeeded
        EnsureSuccess(response);
    }
}

The Problem

So the flow of control and information works fine. The problem is that when API 2 responds with an error, response.ReasonPhrase says "Bad Request" or "Internal Server Error", instead of the message I set in the exception.

Been spitting blood on this for a whole day now. Any insights?

More Info (TLDR)

For clarity, all my APIs have a global exception filter registered to handle errors:

public class RepositoryExceptionsHandlerAttribute : ExceptionFilterAttribute
{
    public override void OnException(HttpActionExecutedContext context)
    {
        HandleException(context);

        base.OnException(context);
    }

    public override Task OnExceptionAsync(HttpActionExecutedContext context, CancellationToken cancellationToken)
    {
        HandleException(context);

        return base.OnExceptionAsync(context, cancellationToken);
    }

    /// <summary>
    /// Recognizes common repository exceptions and if necessary creates a response and updates the context.
    /// </summary>
    /// <param name="context">The context in which the exception was thrown.</param>
    private void HandleException(HttpActionExecutedContext context)
    {
        var response = CreateResponse(context.Request, context.Exception);

        if (response != null)
            context.Response = response;
    }

    /// <summary>
    /// Recognizes common repository exceptions and creates a corresponding error response.
    /// </summary>
    /// <param name="request">The request to which the response should be created.</param>
    /// <param name="ex">The exception to handle.</param>
    /// <returns>An error response containing the status code and exception data, or null if this is not a common exception.</returns>
    private HttpResponseMessage CreateResponse(HttpRequestMessage request, Exception ex)
    {
        string message = ex.Message;

        if (ex is KeyNotFoundException)        return request.CreateErrorResponse(HttpStatusCode.NotFound, message);
        if (ex is ArgumentException)           return request.CreateErrorResponse(HttpStatusCode.BadRequest, message);
        if (ex is InvalidOperationException)   return request.CreateErrorResponse(HttpStatusCode.BadRequest, message);
        if (ex is UnauthorizedAccessException) return request.CreateErrorResponse(HttpStatusCode.Unauthorized, message);
#if !DEBUG
        // For security reasons, when an exception is not handled the system should return a general error, not exposing the real error information
        // In development time, the programmer will need the details of the error, so this general message is disabled.
        request.CreateErrorResponse(HttpStatusCode.InternalServerError, Errors.InternalServerError);
#endif

        return null;
    }
}

This works fine between the user and API 1. But when API 1 and API 2 do their thing, the response creation ignores the message I put in and sets the status as the reason.


Solution

  • The new CreateResponse() method:

        /// <summary>
        /// Recognizes common repository exceptions and creates a corresponding error response.
        /// </summary>
        /// <param name="request">The request to which the response should be created.</param>
        /// <param name="ex">The exception to handle.</param>
        /// <returns>An error response containing the status code and exception data, or null if this is not a common exception.</returns>
        private HttpResponseMessage CreateResponse(HttpRequestMessage request, Exception ex)
        {
            string message = ex.Message;
    
            HttpStatusCode code = 0;
    
            if      (ex is KeyNotFoundException)        code = HttpStatusCode.NotFound;
            else if (ex is ArgumentException)           code = HttpStatusCode.BadRequest;
            else if (ex is InvalidOperationException)   code = HttpStatusCode.BadRequest;
            else if (ex is UnauthorizedAccessException) code = HttpStatusCode.Unauthorized;
            else if (ex is HttpException)
            {
                // HttpExceptions are thrown when request between IdentityServer and the API server have failed.
                // IdentityServer has generated an error, the API server received it and now it needs to relay it back to the client.
                var httpException = (HttpException) ex;
    
                code = (HttpStatusCode) httpException.GetHttpCode();
                message = httpException.Message;
            }
            else
            {
                code = HttpStatusCode.InternalServerError;
    
                // For security reasons, when an exception is not handled the system should return a general error, not exposing the real error information
                // In development time, the programmer will need the details of the error, so this general message is disabled.
    #if DEBUG
                message = ex.Message;
    #else
                message = Errors.InternalServerError;
    #endif
            }
    
            // For some reason the request.CreateErrorResponse() method ignores the message given to it and parses its own message.
            // The error response is constructed manually.
            return CreateErrorResponse(request, code, message);
        }
    
        private HttpResponseMessage CreateErrorResponse(HttpRequestMessage request, HttpStatusCode code, string message)
        {
            var content = new { Message = message };
    
            return new HttpResponseMessage(code)
            {
                ReasonPhrase = message,
                RequestMessage = request,
                Content = new ObjectContent(content.GetType(), content, new JsonMediaTypeFormatter())
            };
        }
    }
    

    Hope this just saved someone's day... ;)