Asp.Net AuthorizationHandler's response upsets Chrome, causes "net::ERR_HTTP2_PROTOCOL_ERROR"
Asked Answered
S

2

0

I've decided to write a custom AuthorizationHandler for a custom Policy I'm using :

// I pass this to AddPolicy in startup.cs
public class MyRequirement : IAuthorizationRequirement {

    public MyRequirement () { ... }
}



public class MyAuthorizationHandler : AuthorizationHandler<MyRequirement> {


    public MyAuthorizationHandler() { }


    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, MyRequirement requirement) {

        if (context.Resource is HttpContext httpContext) {
            var endpoint = httpContext.GetEndpoint();

            if ( /* conditions for hard failure */ ) { context.Fail(); return; }
            
            if ( /* conditions for success */) { context.Succeed(requirement); return; }

            // Neither a success nor a failure, simply a different response.
            httpContext.Response.StatusCode = 404;
            httpContext.Response.ContentType = "application/json";
            await httpContext.Response.WriteAsync("Blah blah NotFound").ConfigureAwait(false);
            return;
        }

        context.Fail();
    }
}

I've seen similar code snippets in other StackOverlflow answers. (e.g. here : How to change status code & add message from failed AuthorizationHandler policy )

Problem : this doesn't seem to generate a "valid" 404 response. I think so for two reasons:

  • When I look at Chrome's network tab, the response is NOT "404", instead it's net::ERR_HTTP2_PROTOCOL_ERROR 404
  • When I look at the response data, there's only headers. My custom error text ("Blah blah NotFound") does not appear anywhere.

What am I doing wrong?

Note : I've tried returning immediately after setting the 404, without doing context.Fail() but I get the same result.

Spain answered 15/11, 2022 at 17:2 Comment(0)
S
2

The root cause:

My Web Api had several middlewares working with the response value. Those middleware were chained up in Startup.cs, using the traditional app.UseXXX().

Chrome was receiving 404 (along with my custom response body) from my Requirement middleware (hurray!), but Chrome is "smart" and by design continues to receive the response even after that initial 404 -- for as long as the server continues generating some response data.

Because of that, Chrome eventually came across a different response added by another of the chained up middlewares. The 404 was still there, but the response body was slightly changed.

And since chrome is paranoid, it would display this net::ERR_HTTP2_PROTOCOL_ERROR to indicate that someone had messed up the consistency of the response somewhere along the chain of responders.

==========

The solution :

Finalize your response with Response.CompleteAsync() to prevent any other subsequent middleware from changing it further :

protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, MyRequirement requirement) {

    if (context.Resource is HttpContext httpContext) {
        var endpoint = httpContext.GetEndpoint();

        if ( /* conditions for hard failure */ ) { context.Fail(); return; }
        
        if ( /* conditions for success */) {
            context.Succeed(requirement);
            return; 
        }

        // Neither a requirement success nor a requirement failure, just a different response : 

        httpContext.Response.StatusCode = 404;
        httpContext.Response.ContentType = "application/json";
        await httpContext.Response.WriteAsync("Blah blah NotFound");
        await httpContext.Response.CompleteAsync(); // <-- THIS!!!
        return;
    }

    context.Fail();
}

Please note : if your 'HandleRequirementAsync' function does not have the 'async' keyword, then do not use 'await' inside of it, and do return Task.CompletedTask; instead of just return;

Spain answered 16/11, 2022 at 14:30 Comment(0)
B
0

Below is a work demo based on your code, you can refer to it.

public class MyAuthorizationHandler : AuthorizationHandler<MyRequirement>
{
    private readonly IHttpContextAccessor httpContextAccessor;

    public MyAuthorizationHandler(IHttpContextAccessor httpContextAccessor) 
    {
        this.httpContextAccessor = httpContextAccessor; 
     }


    protected override  Task HandleRequirementAsync(AuthorizationHandlerContext context, MyRequirement requirement) 
    {

            
            if ( /* conditions for success */) {
                context.Succeed(requirement);
                return; 
            }

            // If it fails at this point, I want to return 404 because of reasons.
            else
           {
            var httpContext = httpContextAccessor.HttpContext;
            httpContext.Response.StatusCode = 404;
            httpContext.Response.ContentType = "application/json";            
            httpContext.Response.WriteAsync("Blah blah NotFound").ConfigureAwait(false);  
           
        }
        return Task.CompletedTask;
    }
}

result:

enter image description here

Brower answered 16/11, 2022 at 10:53 Comment(4)
Did you really try that? I find it hard to believe. For a start, the C# compiler should complain that you return Task.CompletedTask from a function marked "async". I think you missed one key difference between my function and all the examples of HandleRequirementAsync in the code snippets: Mine is marked "async" because it contains a few "await"Spain
Definitely doesn't work. If this was real code, the compiler would also complain that httpContext can be null, at : "httpContext.Response.XXX"Spain
@Spain My mistake, I don't use "await" , so my Mine isn't marked "async". Besides, I use "var httpContext = httpContextAccessor.HttpContext;" so, I can get httpContext .Brower
thanks for the clarifications; all in all those were all syntactic details, the issue lied elsewhere, as pointed out in the solution I posted.Spain

© 2022 - 2024 — McMap. All rights reserved.