Skip to content

Commit f9a064c

Browse files
committed
Fix for codeplex #1996 - Nullref on 404 with webapi batching
The issue here is that one of the requests in a batch was matched by an MVC route when running in web host. The batch handler uses HttpServer to invoke the individual requests. Internally the HttpServer forwards the request to an HttpRoutingDispatcher. In the web host case, the HttpRoutingDispatcher has a reference to HostedHttpRouteCollection - which is implemented by matching BOTH WebAPI and MVC (System.Web) routes. If an MVC route matches, then the produced IHttpRouteData instance doesn't have its .Route property set as the underlying route that matched does not implement IHttpRoute. This causes attribute routing in the controller selector to fail, as we did not anticipate this case. Followup filed to consider whether or not this is the right behavior for the dispatcher/route-collection.
1 parent 476fdc1 commit f9a064c

File tree

2 files changed

+86
-2
lines changed

2 files changed

+86
-2
lines changed

src/System.Web.Http/Routing/HttpRouteDataExtensions.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,22 @@ internal static CandidateAction[] GetDirectRouteCandidates(this IHttpRouteData r
7272
{
7373
Contract.Assert(routeData != null);
7474
IEnumerable<IHttpRouteData> subRoutes = routeData.GetSubRoutes();
75+
76+
// Possible this is being called on a subroute. This can happen after ElevateRouteData. Just chain.
7577
if (subRoutes == null)
7678
{
77-
// Possible this is being called on a subroute. This can happen after ElevateRouteData. Just chain.
78-
return routeData.Route.GetDirectRouteCandidates();
79+
if (routeData.Route == null)
80+
{
81+
// If the matched route is a System.Web.Routing.Route (in web host) then routeData.Route
82+
// will be null. Normally a System.Web.Routing.Route match would go through an MVC handler
83+
// but we can get here through HttpRoutingDispatcher in WebAPI batching. If that happens,
84+
// then obviously it's not a WebAPI attribute routing match.
85+
return null;
86+
}
87+
else
88+
{
89+
return routeData.Route.GetDirectRouteCandidates();
90+
}
7991
}
8092

8193
var list = new List<CandidateAction>();

test/System.Web.Http.Test/Dispatcher/DefaultHttpControllerSelectorTest.cs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
22

3+
using System.Collections.Generic;
34
using System.Collections.ObjectModel;
45
using System.Linq;
56
using System.Net;
@@ -290,6 +291,77 @@ public void SelectController_Throws_NotFound_NoRouteData()
290291
Assert.Equal(HttpStatusCode.NotFound, ex.Response.StatusCode);
291292
}
292293

294+
// We ignore attribute routes when RouteData.Route is null
295+
// See: https://aspnetwebstack.codeplex.com/workitem/1996
296+
[Fact]
297+
public void SelectController_WhenRouteData_RouteIsNull_FindsController()
298+
{
299+
// Arrange
300+
HttpConfiguration configuration = new HttpConfiguration();
301+
Mock<IHttpControllerTypeResolver> controllerTypeResolver = new Mock<IHttpControllerTypeResolver>();
302+
configuration.Services.Replace(typeof(IHttpControllerTypeResolver), controllerTypeResolver.Object);
303+
304+
Type controllerType = GetMockControllerType("Sample");
305+
controllerTypeResolver
306+
.Setup(c => c.GetControllerTypes(It.IsAny<IAssembliesResolver>()))
307+
.Returns(new Collection<Type> { controllerType });
308+
309+
HttpRequestMessage request = new HttpRequestMessage();
310+
311+
var routeValues = new Dictionary<string, object>(StringComparer.Ordinal)
312+
{
313+
{ "controller", "Sample" }
314+
};
315+
316+
var routeData = new Mock<IHttpRouteData>();
317+
routeData.SetupGet(rd => rd.Values).Returns(routeValues);
318+
routeData.SetupGet(rd => rd.Route).Returns((IHttpRoute)null);
319+
request.SetRouteData(routeData.Object);
320+
321+
DefaultHttpControllerSelector selector = new DefaultHttpControllerSelector(configuration);
322+
323+
// Act
324+
var controller = selector.SelectController(request);
325+
326+
// Assert
327+
Assert.Equal(controllerType, controller.ControllerType);
328+
}
329+
330+
// We ignore attribute routes when RouteData.Route is null
331+
// See: https://aspnetwebstack.codeplex.com/workitem/1996
332+
[Fact]
333+
public void SelectController_WhenRouteData_RouteIsNull_DoesNotFindController()
334+
{
335+
// Arrange
336+
HttpConfiguration configuration = new HttpConfiguration();
337+
Mock<IHttpControllerTypeResolver> controllerTypeResolver = new Mock<IHttpControllerTypeResolver>();
338+
configuration.Services.Replace(typeof(IHttpControllerTypeResolver), controllerTypeResolver.Object);
339+
340+
controllerTypeResolver
341+
.Setup(c => c.GetControllerTypes(It.IsAny<IAssembliesResolver>()))
342+
.Returns(new Collection<Type> {}); // No controllers here
343+
344+
HttpRequestMessage request = new HttpRequestMessage();
345+
346+
var routeValues = new Dictionary<string, object>(StringComparer.Ordinal)
347+
{
348+
{ "controller", "Sample" }
349+
};
350+
351+
var routeData = new Mock<IHttpRouteData>();
352+
routeData.SetupGet(rd => rd.Values).Returns(routeValues);
353+
routeData.SetupGet(rd => rd.Route).Returns((IHttpRoute)null);
354+
request.SetRouteData(routeData.Object);
355+
356+
DefaultHttpControllerSelector selector = new DefaultHttpControllerSelector(configuration);
357+
358+
// Act
359+
var ex = Assert.Throws<HttpResponseException>(
360+
() => selector.SelectController(request));
361+
362+
// Assert
363+
Assert.Equal(HttpStatusCode.NotFound, ex.Response.StatusCode);
364+
}
293365

294366
[Fact]
295367
public void SelectController_Throws_NotFound_NoMatchingControllerType()

0 commit comments

Comments
 (0)