Reputation: 75073
Regarding the decorator [AllowHtml]
on a property or even the [ValidateInput(false)]
on a method, what is the best way to catch the HttpRequestValidationException
and append it to the ModelState to show as a friendly error on the user side and not show an error page (either throwing a new page under Application_Error
or make the use of custom error pages.
Inside the global.asax
I have a trap:
protected void Application_Error()
{
// http://romsteady.blogspot.dk/2007/06/how-to-catch-httprequestvalidationexcep.html
// Code that runs when an unhandled error occurs
System.Exception ex = Server.GetLastError();
if (ex is System.Web.HttpRequestValidationException)
{
// I got the exception here, I can do plenty now!
Server.ClearError(); // no need to continue, I know the error
}
}
how do I get from here to the model state, without using any Session/Application variables (thinking about cloud here and all those different server hosting the user request)?
I was thinking add to the route, or TempData
but such is not available here... maybe a Cookie
but seams to hacky...
Any ideas?
Upvotes: 3
Views: 2500
Reputation: 2009
I once handled this situation via a custom ModelBinder and throwing a try/catch around the base.BindModel call. It's ugly, but it gets the job done.
I repeat, it's ugly.
Here's an example:
public class FooModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
Foo model;
try
{
model = (Foo)base.BindModel(controllerContext, bindingContext);
}
catch (HttpRequestValidationException)
{
// handle here
}
}
}
Now, in a valiant effort to populate the ModelState with errors, I have a helper class that does its best to maintain state. Its use (and implementation) leave a lot to be desired (a lot of plumbing, use of magic strings, type-specific, regex of exception message text, etc), so any suggestions are welcome. This is the ugliest part, imo.
Usage:
// from above code snippet
catch (HttpRequestValidationException)
{
// handle any potentially dangerous form values here. Don't want an exception bubbling up to the user
// so handle the HttpRequestValidationException by hand here
// manually populate the model here so that the original values are presented back to the user
model = new Foo()
{
Bar = HandleHttpRequestValidationExceptionHelper.TryAssignment(bindingContext.ModelState, () => bindingContext.ValueProvider.GetValue("Bar").AttemptedValue),
Baz = HandleHttpRequestValidationExceptionHelper.TryAssignment(bindingContext.ModelState, () => bindingContext.ValueProvider.GetValue("Baz").AttemptedValue)
};
}
return model;
The helper does its best to mine out pertinent error information for the user, but it's really crappy. (Notice a theme?)
Implementation:
public static class HandleHttpRequestValidationExceptionHelper
{
/// <summary>
/// Use TryAssignment in anticipation of a HttpRequestValidationException; it's used to help return error information to the user
/// </summary>
/// <param name="modelStateDictionary">The ModelStateDictionary to add the errors to</param>
/// <param name="action">The attempted value to assign</param>
/// <returns>Either the proper value or the errored value read from the HttpRequestValidationException Message property</returns>
public static string TryAssignment(ModelStateDictionary modelStateDictionary, Func<string> action)
{
try
{
return action();
}
catch (HttpRequestValidationException ex)
{
// in effort to better inform the user, try to fish out the offending form field
var parenthesesMatch = Regex.Match(ex.Message, @"\(([^)]*)\)");
if (parenthesesMatch.Success)
{
var badFormInput = parenthesesMatch.Groups[1].Value.Split('=');
modelStateDictionary.AddModelError(badFormInput[0], badFormInput[1] + " is not valid.");
return badFormInput[1].TrimStart('"').TrimEnd('"');
}
else
{
// if attempt to find the offending field fails, just give a general error
modelStateDictionary.AddModelError("", "Please enter valid information.");
return string.Empty;
}
}
}
/// <summary>
/// Use TryAssignment in anticipation of a HttpRequestValidationException; it's used to help return error information to the user
/// </summary>
/// <typeparam name="T">Type of the value</typeparam>
/// <param name="modelStateDictionary">The ModelStateDictionary to add the errors to</param>
/// <param name="action">The attempted value to assign</param>
/// <returns>Either the proper value or default(T)</returns>
public static T TryAssignment<T>(ModelStateDictionary modelState, Func<T> action)
{
try
{
return action();
}
catch (HttpRequestValidationException ex)
{
// in effort to better inform the user, try to fish out the offending form field
var parenthesesMatch = Regex.Match(ex.Message, @"\(([^)]*)\)");
if (parenthesesMatch.Success)
{
var badFormInput = parenthesesMatch.Groups[1].Value.Split('=');
modelState.AddModelError(badFormInput[0], badFormInput[1] + " is not valid.");
// can't really cast a string to an unknown type T. safer to just return default(T)
}
else
{
// if attempt to find the offending field fails, just give a general error
modelState.AddModelError("", "Please enter valid information.");
}
return default(T);
}
}
}
Basically, upon catching an exception, try rebinding the model manually, ready to catch a potential HttpRequestValidationException
error for each property. If one is caught, populate the ModelStateDictionary accordingly with as specific of a message as I can get.
I really wish the framework made it easier to 1) catch this exception and 2) gracefully handle it instead of crapping the entire bed.
Upvotes: 2
Reputation: 6679
Error handling in ASP.NET MVC is a controversial subject. You have different choices to handle errors. Read:
Upvotes: 1