Skip to content

Commit a0513ea

Browse files
Allow file middlewares to run if there's an endpoint with a null request delegate (#42458)
* Allow file middlewares to run if there's an endpoint with a null request delegate #42413 * Ensure EndpointMiddleware checks AuthZ/CORS metadata even if endpoint.RequestDelegate is null * Update null equality check to use "is" * Added tests for StaticFileMiddleware changes * Add tests for DefaultFiles/DirectoryBrowser middleware * Add endpoint middleware test
1 parent d6d2b0c commit a0513ea

8 files changed

+222
-24
lines changed

src/Http/Routing/src/EndpointMiddleware.cs

+19-16
Original file line numberDiff line numberDiff line change
@@ -31,41 +31,44 @@ public EndpointMiddleware(
3131
public Task Invoke(HttpContext httpContext)
3232
{
3333
var endpoint = httpContext.GetEndpoint();
34-
if (endpoint?.RequestDelegate != null)
34+
if (endpoint is not null)
3535
{
3636
if (!_routeOptions.SuppressCheckForUnhandledSecurityMetadata)
3737
{
38-
if (endpoint.Metadata.GetMetadata<IAuthorizeData>() != null &&
38+
if (endpoint.Metadata.GetMetadata<IAuthorizeData>() is not null &&
3939
!httpContext.Items.ContainsKey(AuthorizationMiddlewareInvokedKey))
4040
{
4141
ThrowMissingAuthMiddlewareException(endpoint);
4242
}
4343

44-
if (endpoint.Metadata.GetMetadata<ICorsMetadata>() != null &&
44+
if (endpoint.Metadata.GetMetadata<ICorsMetadata>() is not null &&
4545
!httpContext.Items.ContainsKey(CorsMiddlewareInvokedKey))
4646
{
4747
ThrowMissingCorsMiddlewareException(endpoint);
4848
}
4949
}
5050

51-
Log.ExecutingEndpoint(_logger, endpoint);
52-
53-
try
51+
if (endpoint.RequestDelegate is not null)
5452
{
55-
var requestTask = endpoint.RequestDelegate(httpContext);
56-
if (!requestTask.IsCompletedSuccessfully)
53+
Log.ExecutingEndpoint(_logger, endpoint);
54+
55+
try
5756
{
58-
return AwaitRequestTask(endpoint, requestTask, _logger);
57+
var requestTask = endpoint.RequestDelegate(httpContext);
58+
if (!requestTask.IsCompletedSuccessfully)
59+
{
60+
return AwaitRequestTask(endpoint, requestTask, _logger);
61+
}
5962
}
60-
}
61-
catch (Exception exception)
62-
{
63+
catch (Exception exception)
64+
{
65+
Log.ExecutedEndpoint(_logger, endpoint);
66+
return Task.FromException(exception);
67+
}
68+
6369
Log.ExecutedEndpoint(_logger, endpoint);
64-
return Task.FromException(exception);
70+
return Task.CompletedTask;
6571
}
66-
67-
Log.ExecutedEndpoint(_logger, endpoint);
68-
return Task.CompletedTask;
6972
}
7073

7174
return _next(httpContext);

src/Http/Routing/test/UnitTests/EndpointMiddlewareTest.cs

+29
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,35 @@ public async Task Invoke_WithEndpoint_ThrowsIfAuthAttributesWereFound_ButAuthMid
120120
Assert.Equal(expected, ex.Message);
121121
}
122122

123+
[Fact]
124+
public async Task Invoke_WithEndpointWithNullRequestDelegate_ThrowsIfAuthAttributesWereFound_ButAuthMiddlewareNotInvoked()
125+
{
126+
// Arrange
127+
var expected = "Endpoint Test contains authorization metadata, but a middleware was not found that supports authorization." +
128+
Environment.NewLine +
129+
"Configure your application startup by adding app.UseAuthorization() in the application startup code. " +
130+
"If there are calls to app.UseRouting() and app.UseEndpoints(...), the call to app.UseAuthorization() must go between them.";
131+
var httpContext = new DefaultHttpContext
132+
{
133+
RequestServices = new ServiceProvider()
134+
};
135+
136+
RequestDelegate throwIfCalled = (c) =>
137+
{
138+
throw new InvalidTimeZoneException("Should not be called");
139+
};
140+
141+
httpContext.SetEndpoint(new Endpoint(requestDelegate: null, new EndpointMetadataCollection(Mock.Of<IAuthorizeData>()), "Test"));
142+
143+
var middleware = new EndpointMiddleware(NullLogger<EndpointMiddleware>.Instance, throwIfCalled, RouteOptions);
144+
145+
// Act & Assert
146+
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => middleware.Invoke(httpContext));
147+
148+
// Assert
149+
Assert.Equal(expected, ex.Message);
150+
}
151+
123152
[Fact]
124153
public async Task Invoke_WithEndpoint_WorksIfAuthAttributesWereFound_AndAuthMiddlewareInvoked()
125154
{

src/Middleware/StaticFiles/src/DefaultFilesMiddleware.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public DefaultFilesMiddleware(RequestDelegate next, IWebHostEnvironment hostingE
5959
/// <returns></returns>
6060
public Task Invoke(HttpContext context)
6161
{
62-
if (context.GetEndpoint() == null
62+
if (context.GetEndpoint()?.RequestDelegate is null
6363
&& Helpers.IsGetOrHeadMethod(context.Request.Method)
6464
&& Helpers.TryMatchPath(context, _matchUrl, forDirectory: true, subpath: out var subpath))
6565
{

src/Middleware/StaticFiles/src/DirectoryBrowserMiddleware.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,8 @@ public DirectoryBrowserMiddleware(RequestDelegate next, IWebHostEnvironment host
7575
/// <returns></returns>
7676
public Task Invoke(HttpContext context)
7777
{
78-
// Check if the URL matches any expected paths, skip if an endpoint was selected
79-
if (context.GetEndpoint() == null
78+
// Check if the URL matches any expected paths, skip if an endpoint with a request delegate was selected
79+
if (context.GetEndpoint()?.RequestDelegate is null
8080
&& Helpers.IsGetOrHeadMethod(context.Request.Method)
8181
&& Helpers.TryMatchPath(context, _matchUrl, forDirectory: true, subpath: out var subpath)
8282
&& TryGetDirectoryInfo(subpath, out var contents))

src/Middleware/StaticFiles/src/StaticFileMiddleware.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ public StaticFileMiddleware(RequestDelegate next, IWebHostEnvironment hostingEnv
6666
/// <returns></returns>
6767
public Task Invoke(HttpContext context)
6868
{
69-
if (!ValidateNoEndpoint(context))
69+
if (!ValidateNoEndpointDelegate(context))
7070
{
7171
_logger.EndpointMatched();
7272
}
@@ -91,8 +91,8 @@ public Task Invoke(HttpContext context)
9191
return _next(context);
9292
}
9393

94-
// Return true because we only want to run if there is no endpoint.
95-
private static bool ValidateNoEndpoint(HttpContext context) => context.GetEndpoint() == null;
94+
// Return true because we only want to run if there is no endpoint delegate.
95+
private static bool ValidateNoEndpointDelegate(HttpContext context) => context.GetEndpoint()?.RequestDelegate is null;
9696

9797
private static bool ValidateMethod(HttpContext context)
9898
{

src/Middleware/StaticFiles/test/UnitTests/DefaultFilesMiddlewareTests.cs

+46-1
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ private async Task NoMatch_PassesThrough(string baseUrl, string baseDir, string
7878
}
7979

8080
[Fact]
81-
public async Task Endpoint_PassesThrough()
81+
public async Task Endpoint_With_RequestDelegate_PassesThrough()
8282
{
8383
using (var fileProvider = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, ".")))
8484
{
@@ -107,6 +107,9 @@ public async Task Endpoint_PassesThrough()
107107
});
108108

109109
app.UseEndpoints(endpoints => { });
110+
111+
// Echo back the current request path value
112+
app.Run(context => context.Response.WriteAsync(context.Request.Path.Value));
110113
},
111114
services => { services.AddDirectoryBrowser(); services.AddRouting(); });
112115
using var server = host.GetTestServer();
@@ -117,6 +120,48 @@ public async Task Endpoint_PassesThrough()
117120
}
118121
}
119122

123+
[Fact]
124+
public async Task Endpoint_With_Null_RequestDelegate_Does_Not_PassThrough()
125+
{
126+
using (var fileProvider = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, ".")))
127+
{
128+
using var host = await StaticFilesTestServer.Create(
129+
app =>
130+
{
131+
app.UseRouting();
132+
133+
app.Use(next => context =>
134+
{
135+
// Assign an endpoint with a null RequestDelegate, the default files should still run
136+
context.SetEndpoint(new Endpoint(requestDelegate: null,
137+
new EndpointMetadataCollection(),
138+
"test"));
139+
140+
return next(context);
141+
});
142+
143+
app.UseDefaultFiles(new DefaultFilesOptions
144+
{
145+
RequestPath = new PathString(""),
146+
FileProvider = fileProvider
147+
});
148+
149+
app.UseEndpoints(endpoints => { });
150+
151+
// Echo back the current request path value
152+
app.Run(context => context.Response.WriteAsync(context.Request.Path.Value));
153+
},
154+
services => { services.AddDirectoryBrowser(); services.AddRouting(); });
155+
using var server = host.GetTestServer();
156+
157+
var response = await server.CreateRequest("/SubFolder/").GetAsync();
158+
var responseContent = await response.Content.ReadAsStringAsync();
159+
160+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
161+
Assert.Equal("/SubFolder/default.html", responseContent); // Should be modified and be valid path to file
162+
}
163+
}
164+
120165
[Theory]
121166
[InlineData("", @".", "/SubFolder/")]
122167
[InlineData("", @"./", "/SubFolder/")]

src/Middleware/StaticFiles/test/UnitTests/DirectoryBrowserMiddlewareTests.cs

+40-1
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ private async Task NoMatch_PassesThrough(string baseUrl, string baseDir, string
9595
}
9696

9797
[Fact]
98-
public async Task Endpoint_PassesThrough()
98+
public async Task Endpoint_With_RequestDelegate_PassesThrough()
9999
{
100100
using (var fileProvider = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, ".")))
101101
{
@@ -135,6 +135,45 @@ public async Task Endpoint_PassesThrough()
135135
}
136136
}
137137

138+
[Fact]
139+
public async Task Endpoint_With_Null_RequestDelegate_Does_Not_PassThrough()
140+
{
141+
using (var fileProvider = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, ".")))
142+
{
143+
using var host = await StaticFilesTestServer.Create(
144+
app =>
145+
{
146+
app.UseRouting();
147+
148+
app.Use(next => context =>
149+
{
150+
// Assign an endpoint with a null RequestDelegate, the directory browser should still run
151+
context.SetEndpoint(new Endpoint(requestDelegate: null,
152+
new EndpointMetadataCollection(),
153+
"test"));
154+
155+
return next(context);
156+
});
157+
158+
app.UseDirectoryBrowser(new DirectoryBrowserOptions
159+
{
160+
RequestPath = new PathString(""),
161+
FileProvider = fileProvider
162+
});
163+
164+
app.UseEndpoints(endpoints => { });
165+
},
166+
services => { services.AddDirectoryBrowser(); services.AddRouting(); });
167+
using var server = host.GetTestServer();
168+
169+
var response = await server.CreateRequest("/").GetAsync();
170+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
171+
Assert.Equal("text/html; charset=utf-8", response.Content.Headers.ContentType.ToString());
172+
Assert.True(response.Content.Headers.ContentLength > 0);
173+
Assert.Equal(response.Content.Headers.ContentLength, (await response.Content.ReadAsByteArrayAsync()).Length);
174+
}
175+
}
176+
138177
[Theory]
139178
[InlineData("", @".", "/")]
140179
[InlineData("", @".", "/SubFolder/")]

src/Middleware/StaticFiles/test/UnitTests/StaticFileMiddlewareTests.cs

+82
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Diagnostics;
5+
using System.Globalization;
56
using System.Net;
67
using Microsoft.AspNetCore.Builder;
78
using Microsoft.AspNetCore.Hosting;
89
using Microsoft.AspNetCore.Http;
910
using Microsoft.AspNetCore.Http.Features;
1011
using Microsoft.AspNetCore.TestHost;
1112
using Microsoft.AspNetCore.Testing;
13+
using Microsoft.Extensions.DependencyInjection;
1214
using Microsoft.Extensions.FileProviders;
1315
using Microsoft.Extensions.Hosting;
1416
using Moq;
@@ -193,6 +195,86 @@ private async Task FoundFile_Served(string baseUrl, string baseDir, string reque
193195
}
194196
}
195197

198+
[Fact]
199+
public async Task File_Served_If_Endpoint_With_Null_RequestDelegate_Is_Active()
200+
{
201+
using (var fileProvider = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, ".")))
202+
{
203+
using var host = await StaticFilesTestServer.Create(app =>
204+
{
205+
app.UseRouting();
206+
app.Use((ctx, next) =>
207+
{
208+
ctx.SetEndpoint(new Endpoint(requestDelegate: null, new EndpointMetadataCollection(), "NullRequestDelegateEndpoint"));
209+
return next();
210+
});
211+
app.UseStaticFiles(new StaticFileOptions
212+
{
213+
RequestPath = new PathString(),
214+
FileProvider = fileProvider
215+
});
216+
app.UseEndpoints(endpoints => { });
217+
}, services => services.AddRouting());
218+
using var server = host.GetTestServer();
219+
var requestUrl = "/TestDocument.txt";
220+
var fileInfo = fileProvider.GetFileInfo(Path.GetFileName(requestUrl));
221+
var response = await server.CreateRequest(requestUrl).GetAsync();
222+
var responseContent = await response.Content.ReadAsByteArrayAsync();
223+
224+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
225+
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
226+
Assert.True(response.Content.Headers.ContentLength == fileInfo.Length);
227+
Assert.Equal(response.Content.Headers.ContentLength, responseContent.Length);
228+
Assert.NotNull(response.Headers.ETag);
229+
230+
using (var stream = fileInfo.CreateReadStream())
231+
{
232+
var fileContents = new byte[stream.Length];
233+
stream.Read(fileContents, 0, (int)stream.Length);
234+
Assert.True(responseContent.SequenceEqual(fileContents));
235+
}
236+
}
237+
}
238+
239+
[Fact]
240+
public async Task File_NotServed_If_Endpoint_With_RequestDelegate_Is_Active()
241+
{
242+
var responseText = DateTime.UtcNow.Ticks.ToString(CultureInfo.InvariantCulture);
243+
RequestDelegate handler = async (ctx) =>
244+
{
245+
ctx.Response.ContentType = "text/customfortest+plain";
246+
await ctx.Response.WriteAsync(responseText);
247+
};
248+
249+
using (var fileProvider = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, ".")))
250+
{
251+
using var host = await StaticFilesTestServer.Create(app =>
252+
{
253+
app.UseRouting();
254+
app.Use((ctx, next) =>
255+
{
256+
ctx.SetEndpoint(new Endpoint(handler, new EndpointMetadataCollection(), "RequestDelegateEndpoint"));
257+
return next();
258+
});
259+
app.UseStaticFiles(new StaticFileOptions
260+
{
261+
RequestPath = new PathString(),
262+
FileProvider = fileProvider
263+
});
264+
app.UseEndpoints(endpoints => { });
265+
}, services => services.AddRouting());
266+
using var server = host.GetTestServer();
267+
var requestUrl = "/TestDocument.txt";
268+
269+
var response = await server.CreateRequest(requestUrl).GetAsync();
270+
var responseContent = await response.Content.ReadAsStringAsync();
271+
272+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
273+
Assert.Equal("text/customfortest+plain", response.Content.Headers.ContentType.ToString());
274+
Assert.Equal(responseText, responseContent);
275+
}
276+
}
277+
196278
[Theory]
197279
[MemberData(nameof(ExistingFiles))]
198280
public async Task HeadFile_HeadersButNotBodyServed(string baseUrl, string baseDir, string requestUrl)

0 commit comments

Comments
 (0)