Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NullReferenceException in .NET6 in Development mode #162

Open
ErikPilsits-RJW opened this issue Feb 1, 2022 · 15 comments
Open

NullReferenceException in .NET6 in Development mode #162

ErikPilsits-RJW opened this issue Feb 1, 2022 · 15 comments

Comments

@ErikPilsits-RJW
Copy link

In .NET6 the framework automatically enables the developer exception page, and I think this is causing a conflict. When debugging in VS I see these errors after an exception.

[2022-02-01 14:37:55.288] [ERR] [Hellang.Middleware.ProblemDetails.ProblemDetailsMiddleware] [R:80000015-0002-fc00-b63f-84710c7967bb|T:40a57b54219fb5047a6056567bda7f3c|S:a5a054639c167772] An exception was thrown attempting to execute the problem details middleware.
System.NullReferenceException: Object reference not set to an instance of an object.
at Hellang.Middleware.ProblemDetails.ProblemDetailsMiddleware.WriteProblemDetails(HttpContext context, ProblemDetails details)
at Hellang.Middleware.ProblemDetails.ProblemDetailsMiddleware.HandleException(HttpContext context, ExceptionDispatchInfo edi)

followed shortly by

[2022-02-01 14:37:55.300] [WRN] [Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware] [R:80000015-0002-fc00-b63f-84710c7967bb|T:40a57b54219fb5047a6056567bda7f3c|S:a5a054639c167772] The response has already started, the error page middleware will not be executed.

and the exceptions bubble up all the way to

[Microsoft.AspNetCore.Server.IIS.Core.IISHttpServer] [R:80000015-0002-fc00-b63f-84710c7967bb|T:40a57b54219fb5047a6056567bda7f3c|S:a5a054639c167772] Connection ID "18158513707758387220", Request ID "80000015-0002-fc00-b63f-84710c7967bb": An unhandled exception was thrown by the application.

Best I can tell, the exception is thrown when WriteProblemDetails calls await context.Response.CompleteAsync();, and I believe it has to do with the developer exception page middleware being enabled. I have not found any way in .NET6 to disable the developer exception middleware.

@ErikPilsits-RJW
Copy link
Author

Actually, I'm also seeing this in Production mode when debugging in Visual Studio. I don't seem to be seeing (or at least not logging) the above errors in my dev and prod azure app services.

@ErikPilsits-RJW
Copy link
Author

Ok, last bit of info ... seems to only happen on http status 204 errors. I use NSwag to generate api clients, and a 204 response generates a client-specific exception. Other status codes on the exception type, like 500, seem to be fine. But a 204 causes the null ref exception.

Doc on CompleteAsyc indicates it may throw under certain circumstances. Trying to step into IISHttpContext.FeatureCollection.cs seems to indicate _writeBodyTask is null (which is odd because the line is return _writeBodyTask!; which means it should never be null). But that's getting into unfamiliar territory for me.

@khellang
Copy link
Owner

khellang commented Feb 2, 2022

Hey @ErikPilsits-RJW! Thanks for all the details provided. It sounds like a non-trivial setup. Like, what's the relation with the 204 client-generated exception and your server-side API? Are you calling another API upstream? Do you think you'd be able to produce a repro for this? 🤔

@epilsits
Copy link

epilsits commented Feb 2, 2022

Hi, same Erik.

It's not too difficult actually. My API is calling an upstream API, and I use NSwag to generate the upstream client. It's NSwag's convention to throw client exceptions on anything that isn't success. I am mapping that exception to a status code problem details, and set the status in that lambda. It is after the details instance is returned that the exception occurs.

I only get the null ref when setting a 204 status. 500 is ok, for example.

I will try to create some kind of minimal API repro if I can, not sure it will be today though. Though I believe you'd be able to repro this without any of the upstream - just create a base API, in your controller throw some custom exception, and map that exception to a status code problem details where you set a 204 status.

@khellang
Copy link
Owner

khellang commented Feb 2, 2022

I wonder if this is an ASP.NET Core thing? I.e. in the platform itself? I mean, it doesn't make sense to try to write a body with a 204 status code. I wonder if the AspNetCore Module or Kestrel is actually preventing this somehow.

@khellang
Copy link
Owner

khellang commented Feb 2, 2022

The main issue here, IMO, is throwing exceptions for success status codes. That in itself is just bonkers. I actually encountered the same last week with one of our own NSwag-generated clients 🙈

Do you actually intend to map 2xx status codes to problem responses? Otherwise you could handle the exception case where the status is 204 and return null or catch it further down the stack

@khellang
Copy link
Owner

khellang commented Feb 2, 2022

I think we can rule out both development mode and the DeveloperExceptionPageMiddleware, at least 😅

@ErikPilsits-RJW
Copy link
Author

Yeah, I never liked that about nswag clients, but I don't see a way to avoid that behavior. And a try catch for every client call to trap the 204? Now that's crazy.

Is there a way in the mapping function to return a basic 204, ie just pass it through?

I mean, it doesn't make sense to try to write a body with a 204 status code.

So I thought the same thing, and maybe it was an issue with the response content. In the write details function, the context has a null Content-Length. But it's the same thing for a 500 response with no content, it has a Content-Length of null, so it's not specifically that.

Working on a simple repro now.

@khellang
Copy link
Owner

khellang commented Feb 3, 2022

And a try catch for every client call to trap the 204?

Surely you must be able to do this centrally? HttpClientFactory? DelegatingHandler?

Is there a way in the mapping function to return a basic 204, ie just pass it through?

Not really. You can return null to ignore it, but that will just make it bubble further up the pipeline.

There's a big assumption in the middleware that exceptions are... exceptional and should always be handled as such. An upstream server returning an empty (but 100% successful) response isn't really exceptional 😂

@ErikPilsits-RJW
Copy link
Author

Surely you must be able to do this centrally? HttpClientFactory? DelegatingHandler?

I will probably have to do something like this, another middleware or whatever, unless I can find something in the NSwag pipeline to handle it. In my app code there are not many places that the upstream could actually return a 204, and where those are, I'm try/catch ing those for error handling anyway. I kind of stumbled across this in testing.

So I wrote a minimal api repro, and you're correct. This is a framework thing.

Exception thrown: 'CustomException' in ProblemDetailsRepro.dll
An exception of type 'CustomException' occurred in ProblemDetailsRepro.dll but was not handled in user code
some error message

Hellang.Middleware.ProblemDetails.ProblemDetailsMiddleware: Error: An exception was thrown attempting to execute the problem details middleware.

System.InvalidOperationException: Writing to the response body is invalid for responses with status code 204.
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.FirstWriteAsyncInternal(ReadOnlyMemory`1 data, CancellationToken cancellationToken)
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.WritePipeAsync(ReadOnlyMemory`1 data, CancellationToken cancellationToken)
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpResponseStream.WriteAsync(Byte[] buffer, Int32 offset, Int32 count, CancellationToken cancellationToken)
   at Microsoft.AspNetCore.Watch.BrowserRefresh.ResponseStreamWrapper.WriteAsync(Byte[] buffer, Int32 offset, Int32 count, CancellationToken cancellationToken)
   at System.Text.Json.JsonSerializer.WriteStreamAsync[TValue](Stream utf8Json, TValue value, JsonTypeInfo jsonTypeInfo, CancellationToken cancellationToken)
   at System.Text.Json.JsonSerializer.WriteStreamAsync[TValue](Stream utf8Json, TValue value, JsonTypeInfo jsonTypeInfo, CancellationToken cancellationToken)
   at System.Text.Json.JsonSerializer.WriteStreamAsync[TValue](Stream utf8Json, TValue value, JsonTypeInfo jsonTypeInfo, CancellationToken cancellationToken)
   at Microsoft.AspNetCore.Http.HttpResponseJsonExtensions.WriteAsJsonAsyncSlow[TValue](Stream body, TValue value, JsonSerializerOptions options, CancellationToken cancellationToken)
   at Hellang.Middleware.ProblemDetails.ProblemDetailsMiddleware.WriteProblemDetails(HttpContext context, ProblemDetails details)
   at Hellang.Middleware.ProblemDetails.ProblemDetailsMiddleware.HandleException(HttpContext context, ExceptionDispatchInfo edi)
Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware: Error: An unhandled exception has occurred while executing the request.

CustomException: some error message
   at Program.<>c.<<Main>$>b__0_1(HttpContext _) in C:\Users\Erik.Pilsits\source\repos\ProblemDetailsRepro\ProblemDetailsRepro\Program.cs:line 23
   at Microsoft.AspNetCore.Routing.EndpointMiddleware.Invoke(HttpContext httpContext)
--- End of stack trace from previous location ---
   at Hellang.Middleware.ProblemDetails.ProblemDetailsMiddleware.Invoke(HttpContext context)
   at Hellang.Middleware.ProblemDetails.ProblemDetailsMiddleware.HandleException(HttpContext context, ExceptionDispatchInfo edi)
   at Hellang.Middleware.ProblemDetails.ProblemDetailsMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware: Warning: The response has already started, the error page middleware will not be executed.
Microsoft.AspNetCore.Server.Kestrel: Error: Connection id "0HMF6L3V8NLEV", Request id "0HMF6L3V8NLEV:00000002": An unhandled exception was thrown by the application.

CustomException: some error message
   at Program.<>c.<<Main>$>b__0_1(HttpContext _) in C:\Users\Erik.Pilsits\source\repos\ProblemDetailsRepro\ProblemDetailsRepro\Program.cs:line 23
   at Microsoft.AspNetCore.Routing.EndpointMiddleware.Invoke(HttpContext httpContext)
--- End of stack trace from previous location ---
   at Hellang.Middleware.ProblemDetails.ProblemDetailsMiddleware.Invoke(HttpContext context)
   at Hellang.Middleware.ProblemDetails.ProblemDetailsMiddleware.HandleException(HttpContext context, ExceptionDispatchInfo edi)
   at Hellang.Middleware.ProblemDetails.ProblemDetailsMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
   at Microsoft.WebTools.BrowserLink.Net.BrowserLinkMiddleware.ExecuteWithFilterAsync(IHttpSocketAdapter injectScriptSocket, String requestId, HttpContext httpContext)
   at Microsoft.AspNetCore.Watch.BrowserRefresh.BrowserRefreshMiddleware.InvokeAsync(HttpContext context)
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)

I guess there's not much you could (should) do about this? Thoughts?

Repro

using Hellang.Middleware.ProblemDetails;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.DependencyInjection.Extensions;

var builder = WebApplication.CreateBuilder(args);

var services = builder.Services;

services.TryAddSingleton<IActionResultExecutor<ObjectResult>, ProblemDetailsResultExecutor>();

services.AddProblemDetails(o =>
{
    o.MapToStatusCode<CustomException>(204);

    o.ShouldLogUnhandledException = (ctx, ex, pd) => false;
});

var app = builder.Build();

app.UseProblemDetails();

app.MapGet("/problem-details", _ => throw new CustomException("some error message"));

app.Run();

internal class CustomException : Exception
{
    public CustomException(string message) : base(message)
    { }
}

public class ProblemDetailsResultExecutor : IActionResultExecutor<ObjectResult>
{
    public virtual Task ExecuteAsync(ActionContext context, ObjectResult result)
    {
        ArgumentNullException.ThrowIfNull(context);
        ArgumentNullException.ThrowIfNull(result);

        var executor = Results.Json(result.Value, null, "application/problem+json", result.StatusCode);
        return executor.ExecuteAsync(context.HttpContext);
    }
}

@khellang
Copy link
Owner

khellang commented Feb 3, 2022

But in this case, at least you're getting a proper error message. It might be worth looking into filing an issue for the IIS server to get rid of the NRE and add a proper error message there as well. Yeah, I don't think there's much I can do here, I'm afraid 😞

@ErikPilsits-RJW
Copy link
Author

I ended up shamelessly borrowing the framework of this middleware to write an exception handling middleware, which can swallow all the nswag client 204 exceptions and just return a real 204 in the response. It goes in the pipeline after this middleware.

@epilsits
Copy link

epilsits commented Feb 4, 2022

So ... after thinking about this today, my approach works for me, narrowly, as an API wrapper. But if I was doing more with the client response, I'm back to try/catch on each client call. There's no framework way to centrally catch NSwag exceptions and turn them into normal responses. HttpClient handler is too low level, as the client takes care of deserialization after the client return. There are NSwag issues open for this issue.

@ErikPilsits-RJW
Copy link
Author

I ended up overriding the nswag client generator template to return default(T) on a 204. It's just the cleanest way to do it, until they come up with an official solution / change.

@khellang
Copy link
Owner

khellang commented Feb 4, 2022

That sounds like a decent solution. I'll try to find a minimal repro for the NRE and submit an issue/PR to AspNetCore if I manage to reproduce it reliably. I agree with David that it seems like a bug 🐛

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants