proudmonkey / AutoWrapper

A simple, yet customizable global exception handler and Http response wrapper for ASP.NET Core APIs.
MIT License
677 stars 82 forks source link

when PUT and receive 204 NoContent AutoWrapper will intercepted an Unhandle error. #30

Closed larssonsun closed 4 years ago

larssonsun commented 4 years ago

After sending a PUT request, when the server receive 204 NoContent it will intercepted an error

Put Action in ProductController.cs:

[HttpPut("{productId}")]
        public async Task<IActionResult> UpdateProduct(Guid productId, [FromBody]ProductUpdateDTO productUpdateDTO)
        {
            if (productUpdateDTO == null)
            {
                return BadRequest();
            }

            if (!ModelState.IsValid)
            {
                return new UnprocessableEntityObjectResult(ModelState); // larsson:如果要自定义422之外的响应则需要新建一个类继承UnprocessableEntityObjectResult
            }

            var result = await _repository.TryGetProduct(productId);
            if (!result.hasProduct)
            {
                return NotFound();
            }

            _mapper.Map(productUpdateDTO, result.product);
            _repository.UpdateProduct(result.product);

            if (!await _unitOfWork.SaveAsync())
            {
                return StatusCode(500, "Updating product field.");
            }

            return NoContent();
        }

startup.cs:

...
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            app.UseApiResponseAndExceptionWrapper(new AutoWrapperOptions { ShowStatusCode = true });

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }
...

error:

fail: AutoWrapper.AutoWrapperMiddleware[0]
      [500]: Unhandled Exception occurred. Unable to process the request.
System.InvalidOperationException: Writing to the response body is invalid for responses with status code 204.
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ThrowWritingToResponseBodyNotSupported()
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.Advance(Int32 bytes)
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpResponsePipeWriter.Advance(Int32 bytes)
   at Microsoft.AspNetCore.Http.HttpResponseWritingExtensions.Write(HttpResponse response, String text, Encoding encoding)
   at Microsoft.AspNetCore.Http.HttpResponseWritingExtensions.WriteAsync(HttpResponse response, String text, Encoding encoding, CancellationToken cancellationToken)
   at Microsoft.AspNetCore.Http.HttpResponseWritingExtensions.WriteAsync(HttpResponse response, String text, CancellationToken cancellationToken)
   at AutoWrapper.AutoWrapperMembers.WriteFormattedResponseToHttpContext(HttpContext context, Int32 code, String jsonString, Boolean isError)
   at AutoWrapper.AutoWrapperMembers.HandleNotSuccessRequestAsync(HttpContext context, Object body, Int32 code)
   at AutoWrapper.Base.WrapperBase.InvokeAsyncBase(HttpContext context, AutoWrapperMembers awm)
info: AutoWrapper.AutoWrapperMiddleware[0]
      Source:[::1] Request: PUT http localhost:5000/product/c632690e-1784-45b9-9e98-69ad427575c3  {
        "name":"aaaaa",
        "description":"bbb.",
        "isonsale":false,
        "createtime":"2020-2-17"
} Responded with [204] in 157ms
fail: Microsoft.AspNetCore.Server.Kestrel[13]
      Connection id "0HLTJBD08K61J", Request id "0HLTJBD08K61J:00000002": An unhandled exception was thrown by the application.
System.InvalidOperationException: StatusCode cannot be set because the response has already started.
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ThrowResponseAlreadyStartedException(String value)
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.set_StatusCode(Int32 value)
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.Microsoft.AspNetCore.Http.Features.IHttpResponseFeature.set_StatusCode(Int32 value)
   at Microsoft.AspNetCore.Http.DefaultHttpResponse.set_StatusCode(Int32 value)
   at AutoWrapper.AutoWrapperMembers.WriteFormattedResponseToHttpContext(HttpContext context, Int32 code, String jsonString, Boolean isError)
   at AutoWrapper.AutoWrapperMembers.HandleExceptionAsync(HttpContext context, Exception exception)
   at AutoWrapper.Base.WrapperBase.InvokeAsyncBase(HttpContext context, AutoWrapperMembers awm)
   at AutoWrapper.AutoWrapperMiddleware.InvokeAsync(HttpContext context)
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)
proudmonkey commented 4 years ago

hi @larssonsun,

I'll look into this. Thank for your continued feedback! I appreciate it.

arhen commented 4 years ago

I got similar error to thhis too when trying to response with NoContent() althoug the process is successful.

connection id "0HLTK3N5ROEON", Request id "0HLTK3N5ROEON:00000001": An unhandled exception was thrown by the application.
System.InvalidOperationException: StatusCode cannot be set because the response has already started.
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ThrowResponseAlreadyStartedException(String value)
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.set_StatusCode(Int32 value)
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.Microsoft.AspNetCore.Http.Features.IHttpResponseFeature.set_StatusCode(Int32 value)
   at Microsoft.AspNetCore.Http.DefaultHttpResponse.set_StatusCode(Int32 value)
   at AutoWrapper.AutoWrapperMembers.WriteFormattedResponseToHttpContext(HttpContext context, Int32 code, String jsonString, Boolean isError)
   at AutoWrapper.AutoWrapperMembers.HandleExceptionAsync(HttpContext context, Exception exception)
   at AutoWrapper.Base.WrapperBase.InvokeAsyncBase(HttpContext context, AutoWrapperMembers awm)
   at AutoWrapper.AutoWrapperMiddleware.InvokeAsync(HttpContext context)
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)
proudmonkey commented 4 years ago

Guys,

What version you are at? I'm trying to replicate the issue to no avail. Here's the logs i'm getting:

2020-02-17T22:29:07.6686562-06:00 [INF] () Starting web host
2020-02-17T22:29:09.4620893-06:00 [INF] (AutoWrapper.AutoWrapperMiddleware) Source:[::1] Request: GET https localhost:44321/api/values   Responded with [404] in 250ms
2020-02-17T22:29:13.7926189-06:00 [INF] (AutoWrapper.AutoWrapperMiddleware) Source:[::1] Request: PUT https localhost:44321/api/v1/persons/1  {
  "firstName": "Vianne Maverich",
  "lastName": "Durano",
  "dateOfBirth": "2019-09-27T23:27:50.076Z"
} Responded with [204] in 271ms
2020-02-17T22:29:14.6501480-06:00 [INF] (AutoWrapper.AutoWrapperMiddleware) Source:[::1] Request: PUT https localhost:44321/api/v1/persons/1  {
  "firstName": "Vianne Maverich",
  "lastName": "Durano",
  "dateOfBirth": "2019-09-27T23:27:50.076Z"
} Responded with [204] in 15ms
2020-02-17T22:29:16.3375676-06:00 [INF] (AutoWrapper.AutoWrapperMiddleware) Source:[::1] Request: PUT https localhost:44321/api/v1/persons/1  {
  "firstName": "Vianne Maverich",
  "lastName": "Durano",
  "dateOfBirth": "2019-09-27T23:27:50.076Z"
} Responded with [204] in 12ms
arhen commented 4 years ago

What version you are at? I'm trying to replicate the issue to no avail. Here's the logs i'm getting:

V3.1

proudmonkey commented 4 years ago

I mean version of AutoWrapper. :)

arhen commented 4 years ago

Guys,

What version you are at? I'm trying to replicate the issue to no avail. Here's the logs i'm getting:

2020-02-17T22:29:07.6686562-06:00 [INF] () Starting web host
2020-02-17T22:29:09.4620893-06:00 [INF] (AutoWrapper.AutoWrapperMiddleware) Source:[::1] Request: GET https localhost:44321/api/values   Responded with [404] in 250ms
2020-02-17T22:29:13.7926189-06:00 [INF] (AutoWrapper.AutoWrapperMiddleware) Source:[::1] Request: PUT https localhost:44321/api/v1/persons/1  {
  "firstName": "Vianne Maverich",
  "lastName": "Durano",
  "dateOfBirth": "2019-09-27T23:27:50.076Z"
} Responded with [204] in 271ms
2020-02-17T22:29:14.6501480-06:00 [INF] (AutoWrapper.AutoWrapperMiddleware) Source:[::1] Request: PUT https localhost:44321/api/v1/persons/1  {
  "firstName": "Vianne Maverich",
  "lastName": "Durano",
  "dateOfBirth": "2019-09-27T23:27:50.076Z"
} Responded with [204] in 15ms
2020-02-17T22:29:16.3375676-06:00 [INF] (AutoWrapper.AutoWrapperMiddleware) Source:[::1] Request: PUT https localhost:44321/api/v1/persons/1  {
  "firstName": "Vianne Maverich",
  "lastName": "Durano",
  "dateOfBirth": "2019-09-27T23:27:50.076Z"
} Responded with [204] in 12ms

Are you using return NoContent() at the end?

arhen commented 4 years ago

I mean version of AutoWrapper. :)

Last version, 3.0

proudmonkey commented 4 years ago

Are you using return NoContent() at the end?

Yes:

[Route("{id:long}")]
[HttpPut]
public async Task<IActionResult> Put(long id, [FromBody] PersonDTO dto)
{
            return NoContent();
}

I also tried with GET.

arhen commented 4 years ago

Are you using return NoContent() at the end?

Yes:

[Route("{id:long}")]
[HttpPut]
public async Task<IActionResult> Put(long id, [FromBody] PersonDTO dto)
{
            return NoContent();
}

I also tried with GET.

This is weird. I also tried it using get and got same error. I also try to build AutoWrapper from source code and still got same error. I've tracked the error and explain it a little bit on my PR.

Are you using VS? I'm using Rider. Maybe there are difference debug level information beetween those 2 IDE?

proudmonkey commented 4 years ago

Are you using VS?

Yes.

I don't know what's going on but I've merged your PR. Thank you. It should be included in the next release.

proudmonkey commented 4 years ago

@arhen

Just an FYI, I've updated the conditional check from your PR:

From this:

 if (context.Response.StatusCode != Status304NotModified && context.Response.StatusCode != Status204NoContent)

To this:

 if (context.Response.StatusCode != Status304NotModified || context.Response.StatusCode != Status204NoContent)

The OR conditional statement makes sense as the StatusCodes will be evaluated one at each request.

proudmonkey commented 4 years ago

Just released a new version that comes with a fix for this issue. You can find it here: AutoWrapper Now Supports Problem Details For Your ASP.NET Core APIs

Thank you so much for all your suggestions!

arhen commented 4 years ago

Finally, ProblemDetails 💃