Question ASP.NET Core 6 Web API - How to return 400 bad request responses from middleware

raysefo

Well-known member
Joined
Feb 22, 2019
Messages
361
Programming Experience
10+
Hi guys,
I have an ASP.NET Core 6 Web API that I am calling 3rd party web services. I have exception middleware in order to log exceptions and return 500 responses to hide the details from the user. When this 3rd party web service returns 400 bad request, I also want to return it to the user but something in the request-response middleware makes the response 500 - Internal Server Error. I debugged and realized that after processing this line below I got Internal Server Error.

// Call the next middleware in the pipeline await _next(httpContext);

Here is the request response middleware;
C#:
public class RequestResponseMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly IConfiguration _configuration;
        private readonly bool _isRequestResponseLoggingEnabled;

        public RequestResponseMiddleware(RequestDelegate next, IConfiguration configuration)
        {
            _next = next;
            _configuration = configuration;
            _isRequestResponseLoggingEnabled = configuration.GetValue<bool>("EnableRequestResponseLogging", true);
        }

        public async Task InvokeAsync(HttpContext httpContext)
        {
            // Middleware is enabled only when the EnableRequestResponseLogging config value is set.
            if (_isRequestResponseLoggingEnabled)
            {
                await using var sqlConnection =
                    new SqlConnection(_configuration.GetSection("ConnectionStrings:OyunPalasDbContext").Value);

                await using var cmd =
                    new SqlCommand(
                        "INSERT INTO [dbo].[API_Log] ([Host],[Headers],[StatusCode],[TimeUtc],[RequestBody],[RequestedMethod],[UserHostAddress],[Useragent],[AbsoluteUri],[RequestType]) VALUES (@Host,@Headers,@StatusCode,getdate(),@RequestBody,@RequestedMethod,@UserHostAddress,@Useragent,@AbsoluteUri,@RequestType)",
                        sqlConnection);
                sqlConnection.Open();

                cmd.Parameters.AddWithValue("@Host", httpContext.Request.Host.ToString());
                cmd.Parameters.AddWithValue("@StatusCode", "");
                cmd.Parameters.AddWithValue("@Headers", FormatHeaders(httpContext.Request.Headers));
                cmd.Parameters.AddWithValue("@RequestBody", await ReadBodyFromRequest(httpContext.Request));
                cmd.Parameters.AddWithValue("@RequestedMethod", httpContext.Request.Method);
                cmd.Parameters.AddWithValue("@UserHostAddress", httpContext.Request.Host.ToString());
                cmd.Parameters.AddWithValue("@Useragent", httpContext.Request.Headers["User-Agent"].ToString());
                cmd.Parameters.AddWithValue("@AbsoluteUri", httpContext.Request.Path.ToString());
                cmd.Parameters.AddWithValue("@RequestType", "Request");

                cmd.ExecuteNonQuery();

                var originalResponseBody = httpContext.Response.Body;
                using var newResponseBody = new MemoryStream();
                httpContext.Response.Body = newResponseBody;

                // Call the next middleware in the pipeline
                await _next(httpContext);

                newResponseBody.Seek(0, SeekOrigin.Begin);
                var responseBodyText = await new StreamReader(httpContext.Response.Body).ReadToEndAsync();

                await using var sqlConnectionResponse =
                    new SqlConnection(_configuration.GetSection("ConnectionStrings:OyunPalasDbContext").Value);

                await using var cmdResponse =
                    new SqlCommand(
                        "INSERT INTO [dbo].[API_Log] ([Host],[Headers],[StatusCode],[TimeUtc],[RequestBody],[RequestedMethod],[UserHostAddress],[Useragent],[AbsoluteUri],[RequestType]) VALUES (@Host,@Headers,@StatusCode,getdate(),@RequestBody,@RequestedMethod,@UserHostAddress,@Useragent,@AbsoluteUri,@RequestType)",
                        sqlConnectionResponse);
                sqlConnectionResponse.Open();

                cmdResponse.Parameters.AddWithValue("@Host", httpContext.Request.Host.ToString());
                cmdResponse.Parameters.AddWithValue("@Headers", FormatHeaders(httpContext.Response.Headers));
                cmdResponse.Parameters.AddWithValue("@StatusCode", httpContext.Response.StatusCode.ToString());
                cmdResponse.Parameters.AddWithValue("@RequestBody", responseBodyText);
                cmdResponse.Parameters.AddWithValue("@RequestedMethod", httpContext.Request.Method);
                cmdResponse.Parameters.AddWithValue("@UserHostAddress", httpContext.Request.Host.ToString());
                cmdResponse.Parameters.AddWithValue("@Useragent", "");
                cmdResponse.Parameters.AddWithValue("@AbsoluteUri", "");
                cmdResponse.Parameters.AddWithValue("@RequestType", "Response");

                cmdResponse.ExecuteNonQuery();

                newResponseBody.Seek(0, SeekOrigin.Begin);
                await newResponseBody.CopyToAsync(originalResponseBody);
            }
            else
            {
                await _next(httpContext);
            }
        }

        private static async Task<string> ReadBodyFromRequest(HttpRequest request)
        {
            // Ensure the request's body can be read multiple times (for the next middlewares in the pipeline).
            request.EnableBuffering();

            using var streamReader = new StreamReader(request.Body, leaveOpen: true);
            var requestBody = await streamReader.ReadToEndAsync();

            // Reset the request's body stream position for next middleware in the pipeline.
            request.Body.Position = 0;
            return requestBody;
        }

        private static string FormatHeaders(IHeaderDictionary headers) => string.Join(", ", headers.Select(kvp => $"{{{kvp.Key}: {string.Join(", ", kvp.Value)}}}"));
    }

Here is the exception middleware,
C#:
public class ExceptionMiddleware
    {
        private readonly RequestDelegate _next;
        private static IConfiguration _configuration;
        private static ILogger<ExceptionMiddleware> _logger;

        public ExceptionMiddleware(RequestDelegate next, IConfiguration configuration, ILogger<ExceptionMiddleware> logger)
        {
            _next = next;
            _configuration = configuration;
            _logger = logger;
        }

        public async Task InvokeAsync(HttpContext httpContext)
        {
            try
            {
                await _next(httpContext);
            }
            catch (Exception ex)
            {
                await HandleExceptionAsync(httpContext, ex);
            }
        }

        private static async Task HandleExceptionAsync(HttpContext context, Exception exception)
        {
            try
            {
                _logger.LogError("Web API Exception Handler Error: " + exception);
                await using var sqlConnection =
                    new SqlConnection(_configuration.GetSection("ConnectionStrings:OyunPalasDbContext").Value);
                await using var cmd =
                    new SqlCommand(
                        "INSERT INTO [dbo].[APIError] ([Message],[RequestMethod],[RequestUri],[TimeUtc]) VALUES (@Message, @RequestMethod, @RequestUri, @TimeUtc)",
                        sqlConnection);
                sqlConnection.Open();

                cmd.Parameters.AddWithValue("@Message", exception.Message);
                cmd.Parameters.AddWithValue("@TimeUtc", DateTime.Now);
                cmd.Parameters.AddWithValue("@RequestUri", context.Request.Path.ToString());
                cmd.Parameters.AddWithValue("@RequestMethod", context.Request.Method.ToString());

                if (exception.InnerException != null)
                {
                    cmd.Parameters.AddWithValue("@Message", exception.Message + " " + exception.InnerException);
                    _logger.LogError("Web API Exception Handler Inner Error: " + exception.InnerException);
                }

                cmd.ExecuteNonQuery();
            }
            catch (Exception e)
            {
                _logger.LogError("Web API Exception Handler insert error: " + e.Message);
            }

            if (exception is BadHttpRequestException)
            {
                //400 exception.
                context.Response.ContentType = "application/json";
                context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
                await context.Response.WriteAsync(new ErrorDetails()
                {
                    StatusCode = context.Response.StatusCode,
                    Message = exception.Message
                }.ToString());
            }
            else
            {
                //return 500 error
                context.Response.ContentType = "application/json";
                context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
                await context.Response.WriteAsync(new ErrorDetails()
                {
                    StatusCode = context.Response.StatusCode,
                    Message = "Internal Server Error."
                }.ToString());
            }
        }
    }
In conclusion, even though I get 400 error in the exception middleware, I am getting 500 Internal server error response due to the line I mentioned.
 
If the client experiences a 500 it's usually becuase an unhandled exception occurred. You don't seem to have your await _next wrapped in an exception handler so any exception it raises will end up as a 500. If you want it to be a 400, catch the exception and return a 400?
 
Back
Top Bottom