Reputation: 311425
I have an internet-facing website that's built using MVC4 and I occasionally get error reports from bots or curious users who send requests for incomplete URLs.
For example:
public class ProductController : Controller
{
[HttpGet]
public void View(int id)
{
// ...
/product/view/1
is valid./product/view
is invalid as the argument is not specified.Such invalid requests raise exceptions resembling:
System.ArgumentException: The parameters dictionary contains a null entry
for parameter 'id' of non-nullable type 'System.Int32' for method
'System.Web.Mvc.ActionResult View(Int32)' in 'Foo.ProductController'. An
optional parameter must be a reference type, a nullable type, or be declared
as an optional parameter.
Parameter name: parameters
at System.Web.Mvc.ActionDescriptor.ExtractParameterFromDictionary(ParameterInfo parameterInfo, IDictionary`2 parameters, MethodInfo methodInfo)
at System.Web.Mvc.ReflectedActionDescriptor.<>c__DisplayClass1.<Execute>b__0(ParameterInfo parameterInfo)
...
As the exception message states, I could make the id
argument nullable and check within the action method, but I have many controllers with many actions.
I'd like to return a BadRequest
/NotFound
response to any request that fails to bind arguments to action parameters, and specify this in a single place in code to apply across all controllers.
How can this be done?
Upvotes: 6
Views: 3310
Reputation: 797
You can use the HandleError attribute to handle errors in your application. The HandleError attribute can be specified both at controller level and method level. I have used something like this before:
[HandleError(ExceptionType = typeof(ArgumentException), View = "MissingArgument")]
public ActionResult Test(int id)
{
return View();
}
If you don't want to catch exceptions on a per method basis you can put the attribute on class level instead:
[HandleError(ExceptionType = typeof(ArgumentException), View = "MissingArgument")]
public class HomeController : Controller
{
}
If you want to handle this in a central place you can add it to the FilterConfig class in the App-Start folder:
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
var error = new HandleErrorAttribute();
error.ExceptionType = typeof (ArgumentException);
error.View = "MissingArgument";
filters.Add(error);
}
The MissingArgument view should be located in the Shared view folder. If you want to send a specific HTTP error code back to the client you can put that in the view:
@{
ViewBag.Title = "Error";
Context.Response.StatusCode = (int) HttpStatusCode.BadRequest;
}
<h2>Not found</h2>
Upvotes: 3
Reputation: 311425
One approach that seems to work is to override OnActionExecuted
in the controller (I use a base controller, so would put it there.)
protected override void OnActionExecuted(ActionExecutedContext filterContext)
{
if (filterContext.Exception == null)
return;
// Avoid 'action parameter missing' exceptions by simply returning an error response
if (filterContext.Exception.TargetSite.DeclaringType == typeof(ActionDescriptor) &&
filterContext.Exception.TargetSite.Name == "ExtractParameterFromDictionary")
{
filterContext.ExceptionHandled = true;
filterContext.Result = new HttpStatusCodeResult((int)HttpStatusCode.BadRequest);
}
}
It feels a little uncomfortable to do this as it may break in future versions of the framework. However if it does break, then the site will revert to returning 500's instead of 400's.
Upvotes: 4