Skip to content

Support SkipStatusCodePages on endpoints and authorized routes #38509

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

Merged
merged 3 commits into from
Nov 23, 2021

Conversation

captainsafia
Copy link
Member

@captainsafia captainsafia commented Nov 18, 2021

Background and Motivation

#10317 identifies an issue where the behavior of the AuthorizationMiddleware circumvents the filter logic in SkipStatusCodeAttribute and results in the StatusCodePageMiddleware being executed when it shouldn't and masking the underlying 401 from the AuthorizationMiddleware.

This PR attempts to resolve this issue by removing the dependency on the IFilter execution order from the SkipStatusCodeAttribute by introducing an ISkipStatusCodesMetadata interface.

Proposed API

namespace Microsoft.AspNetCore.Http.Metadata;

public interface ISkipStatusCodePagesMetadata
{
}
- public class SkipStatusCodePagesAttribute : Attribute, IResourceFilter
+ public class SkipStatusCodePagesAttribute : Attribute, IResourceFilter, ISkipStatusCodePagesMetadata
{
}

Usage Examples

app.UseEndpoints(endpoints =>
{
    endpoints.MapGet("/", [SkipStatusCodePages] (c) =>
    {
        c.Response.StatusCode = 404;
        return Task.CompletedTask;
    });
});

Closes #10317

@captainsafia captainsafia added the old-area-web-frameworks-do-not-use *DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels label Nov 18, 2021
@captainsafia captainsafia force-pushed the safia/skip-status-code-pages branch from f767624 to 40b66e7 Compare November 19, 2021 05:22
@@ -41,10 +42,12 @@ public async Task Invoke(HttpContext context)
{
var statusCodeFeature = new StatusCodePagesFeature();
context.Features.Set<IStatusCodePagesFeature>(statusCodeFeature);
var endpoint = context.GetEndpoint();
Copy link
Member

@Kahbazi Kahbazi Nov 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having context.GetEndpoint(); before _next(context) implies that UseStatusCodePages should be after UseRouting which might not be the case everywhere, including the sample in the docs.

This could be after _next(context) and also only be called if statusCodeFeature.Enabled is true.

Copy link
Member

@Tratcher Tratcher Nov 22, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a fair consideration, and we would want to make sure the doc was accurate.

Do we have examples of anything else that's endpoint aware but placed before UseRouting?

This ordering was recommended so that the attribute behavior could be applied early and then overridden by other middleware/code while processing the request. That's consistent with how the filter works today. If placed after _next, you wouldn't be able to tell if IStatusCodePagesFeature had the default value or if it was set by something in the app.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Tratcher Sorry, I'm confused a little bit! Are you saying that going forward StatusCodePagesMiddleware should always be after RoutingMiddleware?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

StatusCodePagesMiddleware can stay where it is and continue to work for existing apps, but if you want it to be route aware and use the SkipStatusCodePages attribute then it will need to be placed after routing.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see! The implementation has been changed after I add the original comment. It's all clear now.

I assume if StatusCodePagesMiddleware with re-execute would be used after routing it will automatically add the routing, right?

if (app.Properties.TryGetValue(globalRouteBuilderKey, out var routeBuilder) && routeBuilder is not null)
{
return app.Use(next =>
{
RequestDelegate? newNext = null;
// start a new middleware pipeline
var builder = app.New();
// use the old routing pipeline if it exists so we preserve all the routes and matching logic
// ((IApplicationBuilder)WebApplication).New() does not copy globalRouteBuilderKey automatically like it does for all other properties.
builder.Properties[globalRouteBuilderKey] = routeBuilder;
builder.UseRouting();

What if StatusCodePagesFeature set the Enable property lazily? In this case it doesn't matter where StatusCodePagesMiddleware is as long as the feature is accessed after routing, right?

public class StatusCodePagesFeature : IStatusCodePagesFeature
{
    private readonly HttpContext httpContext;
    public StatusCodePagesFeature(HttpContext httpContext)
    {
        _httpContext = httpContext;
    }

    private bool? _enabled;
    public bool Enabled
    {
        get 
        {
            if (_enabled == null)
            {
                var endpoint = _httpContext.GetEndpoint();
                var skipStatusCodePageMetadata = endpoint?.Metadata.GetMetadata<ISkipStatusCodePagesMetadata>();
                if (skipStatusCodePageMetadata is not null)
                {
                    _enabled = false;
                }
                else
                {
                    _enabled = true;
                }
            }
            
            return _enabled;
        }
        set
        {
            _enabled = value;
        }
    }
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

StatusCodePagesFeature is a public class, so if we were to change it, we'd have to make it continue to work as it does today when called with the empty constructor. I think it might be worthwhile to just use a new internal IStatusCodePagesFeature that lazily reads the endpoint.

The only thing I don't like is locking in the wrong value for Enabled if the property is read before UseRouting(). You could definitely argue it's no worse than today where it's locked in immediately when the feature is added, but that feels a little less surprising. I wonder if it'd be better to avoid negative caching unless the property is explicitly set, but maybe that's even more surprising.

@captainsafia captainsafia added the api-ready-for-review API is ready for formal API review - https://github.com/dotnet/apireviews label Nov 20, 2021
@ghost
Copy link

ghost commented Nov 20, 2021

Thank you for your API proposal. I'm removing the api-ready-for-review label. API Proposals should be submitted for review through Issues based on this template.

@ghost ghost removed the api-ready-for-review API is ready for formal API review - https://github.com/dotnet/apireviews label Nov 20, 2021
@captainsafia captainsafia marked this pull request as ready for review November 20, 2021 04:33
@captainsafia captainsafia requested a review from a team November 20, 2021 04:33
@captainsafia captainsafia enabled auto-merge (squash) November 22, 2021 23:37

/// <summary>
/// Defines a contract used to specify metadata for skipping the StatusCodePage
/// middleware in <see cref="Endpoint.Metadata"/>.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we drop the in Endpoint.Metadata? I don't know if it helps clarify things?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the in Endpoint.Metadata. What's wrong with it? It could point the curious to API docs for endpoint routing.

@@ -283,4 +284,38 @@ public async Task Reexecute_WorksAfterUseRoutingWithGlobalRouteBuilder()
var content = await response.Content.ReadAsStringAsync();
Assert.Equal("errorPage", content);
}

[Fact]
public async Task SkipStatusCodePages_SupportsEndpoints()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add a test that verifies we let people change the feature in the body of the endpoint?

endpoints.MapGet("/", [SkipStatusCodePages] (c) =>
{
         c.GetFeature<IStatusCodePagesFeature>().Enabled = false;
});

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Except verify that you can set it to true! Although it really would be weird to reenable it after adding the attribute.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems fair! I missed seeing this before the auto-merge kicked it but I'll address in a follow-up.

@Tratcher
Copy link
Member

Tratcher commented Nov 23, 2021

I assume if StatusCodePagesMiddleware with re-execute would be used after routing it will automatically add the routing, right?

@captainsafia, @Kahbazi's comment makes me realize we designed this wrong. UseStatusCodePagesWithReExecute must go before UseRouting to work, but it must go after routing for [SkipStatusCodePages] to work. Both are not possible. We will have to re-think the ordering requirements. Either re-open #10317 or file a new issue please.

@captainsafia
Copy link
Member Author

@Kahbazi's comment makes me realize we designed this wrong. UseStatusCodePagesWithReExecute must go before UseRouting to work, but it must go after routing for [SkipStatusCodePages] to work. Both are not possible. We will have to re-think the ordering requirements. Either re-open #10317 or file a new issue please.

Hm. Will dig into this to understand the flow here and file a new issue.

@github-actions github-actions bot locked and limited conversation to collaborators Dec 8, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
old-area-web-frameworks-do-not-use *DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

SkipStatusCodePagesAttribute should run before AuthorizeAttribute
6 participants