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

Update middleware that assumes UseRouting is called after them, for minimal hosting #35426

Merged
merged 14 commits into from Aug 24, 2021
@@ -1,9 +1,11 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Diagnostics;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Microsoft.AspNetCore.Builder
Expand All @@ -26,7 +28,7 @@ public static IApplicationBuilder UseExceptionHandler(this IApplicationBuilder a
throw new ArgumentNullException(nameof(app));
}

return app.UseMiddleware<ExceptionHandlerMiddleware>();
return SetExceptionHandlerMiddleware(app, options: null);
}

/// <summary>
Expand Down Expand Up @@ -95,7 +97,53 @@ public static IApplicationBuilder UseExceptionHandler(this IApplicationBuilder a
throw new ArgumentNullException(nameof(options));
}

return app.UseMiddleware<ExceptionHandlerMiddleware>(Options.Create(options));
var iOptions = Options.Create(options);
return SetExceptionHandlerMiddleware(app, iOptions);
}

private static IApplicationBuilder SetExceptionHandlerMiddleware(IApplicationBuilder app, IOptions<ExceptionHandlerOptions>? options)
{
// Check if UseRouting() has been called so we know if it's safe to call UseRouting()
BrennanConroy marked this conversation as resolved.
Show resolved Hide resolved
// otherwise we might call UseRouting() when AddRouting() hasn't been called which would fail
if (app.Properties.TryGetValue("__EndpointRouteBuilder", out _) || app.Properties.TryGetValue("__GlobalEndpointRouteBuilder", out _))
BrennanConroy marked this conversation as resolved.
Show resolved Hide resolved
{
return app.Use(next =>
{
var loggerFactory = app.ApplicationServices.GetRequiredService<ILoggerFactory>();
var diagnosticListener = app.ApplicationServices.GetRequiredService<DiagnosticListener>();

if (options is null)
{
options = app.ApplicationServices.GetRequiredService<IOptions<ExceptionHandlerOptions>>();
}

if (!string.IsNullOrEmpty(options.Value.ExceptionHandlingPath) && options.Value.ExceptionHandler is null)
{
app.Properties.TryGetValue("__GlobalEndpointRouteBuilder", out var routeBuilder);
BrennanConroy marked this conversation as resolved.
Show resolved Hide resolved
// start a new middleware pipeline
var builder = app.New();
if (routeBuilder is not null)
{
// use the old routing pipeline if it exists so we preserve all the routes and matching logic
builder.Properties["__GlobalEndpointRouteBuilder"] = routeBuilder;
BrennanConroy marked this conversation as resolved.
Show resolved Hide resolved
}
builder.UseRouting();
// apply the next middleware
builder.Run(next);
// store the pipeline for the error case
options.Value.ExceptionHandler = builder.Build();
}

return new ExceptionHandlerMiddleware(next, loggerFactory, options, diagnosticListener).Invoke;
});
}

if (options is null)
{
return app.UseMiddleware<ExceptionHandlerMiddleware>();
}

return app.UseMiddleware<ExceptionHandlerMiddleware>(options);
}
}
}
251 changes: 251 additions & 0 deletions src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerTest.cs
Expand Up @@ -657,5 +657,256 @@ public async Task ExceptionHandler_CanReturn404Responses_WhenAllowed()
&& w.EventId == 4
&& w.Message == "No exception handler was found, rethrowing original exception.");
}

[Fact]
public async Task ExceptionHandlerWithPathWorksAfterUseRoutingIfGlobalRouteBuilderUsed()
{
using var host = new HostBuilder()
.ConfigureWebHost(webHostBuilder =>
{
webHostBuilder
.ConfigureServices(services =>
{
services.AddRouting();
})
.UseTestServer()
.Configure(app =>
{
app.Use(async (httpContext, next) =>
{
Exception exception = null;
try
{
await next(httpContext);
}
catch (InvalidOperationException ex)
{
exception = ex;
}

Assert.Null(exception);
});

app.UseRouting();
app.Properties["__GlobalEndpointRouteBuilder"] = app.Properties["__EndpointRouteBuilder"];

app.UseExceptionHandler("/handle-errors");

app.UseEndpoints(endpoints =>
{
endpoints.Map("/handle-errors", c => {
c.Response.StatusCode = 200;
return c.Response.WriteAsync("Handled");
});
});

app.Run((httpContext) =>
{
throw new InvalidOperationException("Something bad happened");
});
});
}).Build();

await host.StartAsync();

using (var server = host.GetTestServer())
{
var client = server.CreateClient();
var response = await client.GetAsync(string.Empty);
response.EnsureSuccessStatusCode();
Assert.Equal("Handled", await response.Content.ReadAsStringAsync());
}
}

[Fact]
public async Task ExceptionHandlerWithOptionsWorksAfterUseRoutingIfGlobalRouteBuilderUsed()
{
using var host = new HostBuilder()
.ConfigureWebHost(webHostBuilder =>
{
webHostBuilder
.ConfigureServices(services =>
{
services.AddRouting();
})
.UseTestServer()
.Configure(app =>
{
app.Use(async (httpContext, next) =>
{
Exception exception = null;
try
{
await next(httpContext);
}
catch (InvalidOperationException ex)
{
exception = ex;
}

Assert.Null(exception);
});

app.UseRouting();
app.Properties["__GlobalEndpointRouteBuilder"] = app.Properties["__EndpointRouteBuilder"];

app.UseExceptionHandler(new ExceptionHandlerOptions()
{
ExceptionHandlingPath = "/handle-errors"
});

app.UseEndpoints(endpoints =>
{
endpoints.Map("/handle-errors", c => {
c.Response.StatusCode = 200;
return c.Response.WriteAsync("Handled");
});
});

app.Run((httpContext) =>
{
throw new InvalidOperationException("Something bad happened");
});
});
}).Build();

await host.StartAsync();

using (var server = host.GetTestServer())
{
var client = server.CreateClient();
var response = await client.GetAsync(string.Empty);
response.EnsureSuccessStatusCode();
Assert.Equal("Handled", await response.Content.ReadAsStringAsync());
}
}

[Fact]
public async Task ExceptionHandlerWithAddWorksAfterUseRoutingIfGlobalRouteBuilderUsed()
{
using var host = new HostBuilder()
.ConfigureWebHost(webHostBuilder =>
{
webHostBuilder
.ConfigureServices(services =>
{
services.AddRouting();
services.AddExceptionHandler(o => o.ExceptionHandlingPath = "/handle-errors");
})
.UseTestServer()
.Configure(app =>
{
app.Use(async (httpContext, next) =>
{
Exception exception = null;
try
{
await next(httpContext);
}
catch (InvalidOperationException ex)
{
exception = ex;
}

Assert.Null(exception);
});

app.UseRouting();
app.Properties["__GlobalEndpointRouteBuilder"] = app.Properties["__EndpointRouteBuilder"];

app.UseExceptionHandler();

app.UseEndpoints(endpoints =>
{
endpoints.Map("/handle-errors", c => {
c.Response.StatusCode = 200;
return c.Response.WriteAsync("Handled");
});
});

app.Run((httpContext) =>
{
throw new InvalidOperationException("Something bad happened");
});
});
}).Build();

await host.StartAsync();

using (var server = host.GetTestServer())
{
var client = server.CreateClient();
var response = await client.GetAsync(string.Empty);
response.EnsureSuccessStatusCode();
Assert.Equal("Handled", await response.Content.ReadAsStringAsync());
}
}

[Fact]
public async Task ExceptionHandlerWithExceptionHandlerNotReplacedWithGlobalRouteBuilder()
{
using var host = new HostBuilder()
.ConfigureWebHost(webHostBuilder =>
{
webHostBuilder
.ConfigureServices(services =>
{
services.AddRouting();
})
.UseTestServer()
.Configure(app =>
{
app.Use(async (httpContext, next) =>
{
Exception exception = null;
try
{
await next(httpContext);
}
catch (InvalidOperationException ex)
{
exception = ex;
}

Assert.Null(exception);
});

app.UseRouting();
app.Properties["__GlobalEndpointRouteBuilder"] = app.Properties["__EndpointRouteBuilder"];

app.UseExceptionHandler(new ExceptionHandlerOptions()
{
ExceptionHandler = httpContext =>
{
httpContext.Response.StatusCode = StatusCodes.Status404NotFound;
return httpContext.Response.WriteAsync("Custom handler");
}
});

app.UseEndpoints(endpoints =>
{
endpoints.Map("/handle-errors", c => {
c.Response.StatusCode = 200;
return c.Response.WriteAsync("Handled");
});
});

app.Run((httpContext) =>
{
throw new InvalidOperationException("Something bad happened");
});
});
}).Build();

await host.StartAsync();

using (var server = host.GetTestServer())
{
var client = server.CreateClient();
var response = await client.GetAsync(string.Empty);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
Assert.Equal("Custom handler", await response.Content.ReadAsStringAsync());
}
}
}
}
Expand Up @@ -20,6 +20,7 @@
<Reference Include="Microsoft.Extensions.FileProviders.Abstractions" />
<Reference Include="Microsoft.Extensions.Logging.Abstractions" />
<Reference Include="Microsoft.Extensions.Options" />
<Reference Include="Microsoft.AspNetCore.Routing" />
</ItemGroup>

</Project>