3

I have an ASP.NET Core API calling a second API.

I throw an exception in my services layer, if there is an error from the second API:

var response = await httpClient.SendAsync(request); //call second API

if (!response.IsSuccessStatusCode)
{
    //return HTTP response with StatusCode = X, if response.StatusCode == X
    throw new HttpRequestException(await response.Content.ReadAsStringAsync()); 
    //this always returns 400
}

How can I throw an exception that will return a response with the same status code from the second API call?

If I use HttpRequestException it will always return 400, even if the response object had StatusCode = 500.

EDIT: The first API endpoint looks like this:

            public async Task<ActionResult<HttpResponseMessage>> CreateTenancy([FromBody]TenancyRequest tenancy)
            {
                //Make some calls...
                return Created(string.Empty, new { TenancyID = newTenancyExternalId });
            }

The second API endpoint looks like this:

    [HttpPost]
    public IHttpActionResult CreateTenancy([FromBody]TenancyDTO tenancyDTO)
    {    
        var tenancy = GetTenancy();    
        return Created(string.Empty, tenancy);
    }

I've tried using throw new HttpResponseException(response); but this removes the descriptive Exception message, the payload ends up like this:

{
    "Code": 500,
    "CorrelationId": "2df08016-e5e3-434a-9136-6824495ed907",
    "DateUtc": "2020-01-30T02:02:48.4428978Z",
    "ErrorMessage": "Processing of the HTTP request resulted in an exception. Please see the HTTP response returned by the 'Response' property of this exception for details.",
    "ErrorType": "InternalServerError"
}

I'd like to keep the ErrorMessage value in the original payload:

{
    "Code": 400,
    "CorrelationId": "ff9466b4-8c80-4dab-b5d7-9bba1355a567",
    "DateUtc": "2020-01-30T03:05:13.2397543Z",
    "ErrorMessage": "\"Specified cast is not valid.\"",
    "ErrorType": "BadRequest"
}

The end goal is to have this returned:

{
    "Code": 500,
    "CorrelationId": "ff9466b4-8c80-4dab-b5d7-9bba1355a567",
    "DateUtc": "2020-01-30T03:05:13.2397543Z",
    "ErrorMessage": "\"Specified cast is not valid.\"",
    "ErrorType": "InternalServerError"
}
David Klempfner
  • 8,700
  • 20
  • 73
  • 153

3 Answers3

2

I tried something simple as changing the return type of the API endpoint and returning the object as it when there is an error. Otherwise, build your own HttpResponseMessage and return that. This snippet below uses text but you can use a serializer to serialize other content if you have.

public async Task<HttpResponseMessage> Test(string str)
{
    var httpClient = new HttpClient();
    var request = new HttpRequestMessage(HttpMethod.Get, $"myAPI that returns different errors 400, 404, 500 etc based on str");

    var response = await httpClient.SendAsync(request);
    if (!response.IsSuccessStatusCode)
        return response;

    // do something else
    return new HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new StringContent("Your Text here") };
}

Other approach of using Filters

The other approach of using IHttpActionResult as your return type, you can use Filters to conform all your HttpResponseMessages to IHttpActionResult.

Filter: Create a separate cs file and use this filter definition.

public class CustomObjectResponse : IHttpActionResult
{
    private readonly object _obj;

    public CustomObjectResponse(object obj)
    {
        _obj = obj;
    }

    public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
    {
        HttpResponseMessage response = _obj as HttpResponseMessage;
        return Task.FromResult(response);
    }
}

and in your API, you would use your filter like so,

public async Task<IHttpActionResult> Test(string str)
{
    var httpClient = new HttpClient();
    var request = new HttpRequestMessage(HttpMethod.Get, $"http://localhost:4500/api/capacity/update-mnemonics/?mnemonic_to_update={str}");

    var response = await httpClient.SendAsync(request);
    if (!response.IsSuccessStatusCode)
        return new CustomObjectResponse(response);

    // Other Code here

    // Return Other objects 
    KeyValuePair<string, string> testClass = new KeyValuePair<string, string>("Sheldon", "Cooper" );
    return new OkWithObjectResult(testClass);

    // Or Return Standard HttpResponseMessage
    return Ok();

}
Jawad
  • 11,028
  • 3
  • 24
  • 37
1

You could simply make your API call and copy its response code into something compatible with IStatusCodeActionResult.

An alternative s to throw a custom exception. Create something like

public class ApiCallException : Exception
{
    public APiCallException(int statusCode, ...)
    {
        ApiStatusCode = statusCode;
    }

    int ApiStatusCode { get; }
    ...
}

and copy over the status code from your API result, and then throw the exception.

var response = await httpClient.SendAsync(request); //call second API
if (!response.IsSuccessStatusCode)
{   
    var content = await response.Content.ReadAsStringAsync();
    throw new ApiCallException(500, content); 
}

You can then register an exception filter to deal with the result when calling AddMvc.

services.AddMvc(options => options.Filters.Add<ExceptionFilter>());

where ExceptionFilter could be something like

public class ExceptionFilter : IExceptionFilter
{
    // ...

    public void OnException(ExceptionContext context)
    {
        if (context.Exception is ApiCallException ace)
        {
            var returnObject = CreateReturnObjectSomehow();
            context.Result = new ObjectResult(returnObject) { StatusCode = ace.StatusCode };
        }
        else
        {
            // do something else
        }
    }
}
Kit
  • 20,354
  • 4
  • 60
  • 103
0

Thanks Jawad and Kit for providing great answers which helped me work out the solution below:

Turns out there was some middleware handling the exception:

    public async Task Invoke(HttpContext httpContext)
    {
        try
        {
            await _next(httpContext);
        }
        catch (Exception exception)
        {
            if (httpContext.Response.HasStarted) throw;

            var statusCode = ConvertExceptionToHttpStatusCode(exception);

            httpContext.Response.Clear();
            httpContext.Response.StatusCode = (int)statusCode;
            httpContext.Response.ContentType = "application/json";

            if (statusCode != HttpStatusCode.BadRequest)
            {
                _logger.Error(exception, "API Error");
            }

            await httpContext.Response.WriteAsync(JsonConvert.SerializeObject(new Error(statusCode, httpContext.Request.CorrelationId(), exception.Message, statusCode.ToString())));
        }
    }

The Error class looks like this:

    public class Error
    {
        public int Code { get; }
        public Guid? CorrelationId { get; }
        public DateTime DateUtc { get; }
        public string ErrorMessage { get; }
        public string ErrorType { get; }

        public Error(HttpStatusCode code, Guid? correlationId, string errorMessage, string errorType)
        {
            Code = (int)code;
            CorrelationId = correlationId;
            DateUtc = DateTime.UtcNow;
            ErrorMessage = errorMessage;
            ErrorType = errorType;
        }
    }

I created this class:

public class ApiCallException : Exception
{
    public int StatusCode { get; }
    public override string Message { get; }
    public ApiCallException(int statusCode, string message)
    {
        StatusCode = statusCode;
        Message = message;
    }
}

Then updated my original code to have this:

                if (!response.IsSuccessStatusCode)
                {
                    throw new ApiCallException((int)response.StatusCode, await response.Content.ReadAsStringAsync());
                }
David Klempfner
  • 8,700
  • 20
  • 73
  • 153
  • Is the function `ConvertExceptionToHttpStatusCode` custom one, don't find it in .net docs. Another SO [question](https://stackoverflow.com/questions/71469376/how-to-get-http-status-code-from-exception) looking for it.. – Anand Sowmithiran Mar 14 '22 at 14:38
  • @AnandSowmithiran I'm not sure sorry. I can't even remember what this project was anymore, it was ages ago. – David Klempfner Mar 15 '22 at 02:43