I am building an MVC 3 application with an IIS 7.5 backend. On my controller, I have action methods that allow the user to add/edit domain objects. The Action handles HTTP Post, has a return value of string which contains any validation error messages encountered during the save process. Here is an example of one Action Method:
[HttpPost]
public string CustomerEdit(CustomerModel customerModel)
{
var errorMessages = new StringBuilder();
var serverErrors = new List<string>();
//Map to a customer domain object
Mapper.CreateMap<CustomerModel, Customer>();
var customer = Mapper.Map<CustomerModel, Customer>(customerModel);
if (customerModel.Oper == "del")
{
var customerIds = new List<Guid>();
customerIds.Add(customer.Id);
if (!_serverService.DeleteCustomers(customerIds))
{
errorMessages.Append("The item could not be deleted");
Response.StatusCode = Constants.STATUS_SERVER_ERROR;
}
}
else
{
//Validate
if (!_serverService.ValidateCustomer(customer, out serverErrors))
{
foreach (var error in serverErrors)
{
ModelState.AddModelError("Validation", error);
}
}
//Save
if (ModelState.IsValid)
{
var saveStatus = _serverService.SaveCustomer(ref customer, _id);
if (!saveStatus)
{
errorMessages.Append("The server encountered and error during Save");
Response.StatusCode = Constants.STATUS_SERVER_ERROR;
}
}
else
{
errorMessages.Append(GetValidationErrors(ModelState));
Response.StatusCode = Constants.STATUS_SERVER_ERROR;
}
}
return errorMessages.ToString();
}
In the case of an error, I need to set the Response.StatusCode property to a value of either 400/500, and return a concatenated string of detailed error messages. Unfortunately, IIS always strips my error string out of the response test, and (in the case of 400 errors) adds replaces it with the string 'Bad Request'
Is there a way to configure IIS to return a custom, Action-specific, string when the status code is set to 400?
After talking to a friend of mine who is a wiz at configuring IIS, I found that in IIS 7+ you can add the following to web.config:
<system.webServer>
<httpErrors errorMode="Detailed"/>
</system.webServer>
If this setting in web.config is used, AND you set the body of the response, then the response body will reach the client. If you do NOT set the response body, then IIS will serve up a detailed error page with detailed error information (see http://learn.iis.net/page.aspx/267/how-to-use-http-detailed-errors-in-iis/). Many folks consider this a security risk, so use with caution.
In MVC it is also possible to do this on a Action by Action basis. See TrySkipisCustomErrors.
Use:
Response.TrySkipIisCustomErrors = true;
Response.StatusCode = (int)HttpStatusCode.BadRequest;
return this.Json(SomeJsonObject); // could output string too
we ended up with going for
<system.webServer>
<httpErrors errorMode="Custom" existingResponse="PassThrough" />
</system.webServer>
in the web.config. Which allowed custom errors that set status codes to leave IIS unmolested.
At least the page Configuring HTTP Error Responses in IIS 7 says
Note
You cannot customize the following HTTP error messages: 400,
403.9, 411, 414, 500, 500.11, 500.14, 500.15, 501, 503, and 505.
EDIT: though in the responses of this question, which looks quite similar, there is a response claiming that at least something can be done with httpErrors configuration.
Related
The default ASP.NET Web Api Core behaviour for unauthorized request is to send 401/403 error with empty content. I'd like to change it by specifying some kind of Json response specifying the error.
But I struggle to find a right place where I can introduce these changes. Official documentation is of no help (read it all). I had a guess that may be I could catch UnathorizedException in my exception filter / middleware but it didn't work out (I guess it gets handled at authorization level or even not thrown at all).
So my question is how can I customize response behavior in case of unauthorized request.
With .Net Core 3 (or may be earlier as well) you can write a middleware to check if the context.Response has a status of 40x and then return a custom object. Below is roughly how I did it:
if (context.Response.StatusCode == (int)HttpStatusCode.Unauthorized)
{
var result = new MyStandardApiResponseDto
{
Error = new MyErrorDto
{
Title = "Unauthorized",
Messages = new List<string> { "You are not authorized to access the resource. Please login again." },
},
Result = null
};
await context.Response.WriteAsync(JsonConvert.SerializeObject(result));
}
In my MVC 5 web application when the user session expires and the user clicks in something in the page that does an AJAX call I was still getting response status 200 with X-Responded-JSON:401.
I have then read and investigated and found a way to solve the problem.
So...added to the Startup class ConfigureAuth method this
app.UseCookieAuthentication(
new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider =
new CookieAuthenticationProvider
{
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(TimeSpan.FromMinutes(15), (manager, user) => user.GenerateUserIdentityAsync(manager)),
OnApplyRedirect = ctx =>
{
if (!IsAjaxRequest(ctx.Request))
{
ctx.Response.Redirect(ctx.RedirectUri);
}
}
},
SlidingExpiration = false,
ExpireTimeSpan = TimeSpan.FromMinutes(30)
});
Just a helper...
private static bool IsAjaxRequest(IOwinRequest request)
{
IReadableStringCollection query = request.Query;
if ((query != null) && (query["X-Requested-With"] == "XMLHttpRequest"))
{
return true;
}
IHeaderDictionary headers = request.Headers;
return (headers != null) && (headers["X-Requested-With"] == "XMLHttpRequest");
}
This solved my problem locally (in my dev machine) and I am now able to see the 401 in the Ajax calls when the sessions expires or the user logs or when is still trying to access something in a open tab.
My problem is that this work only locally. When I have my application in a server this Ajax calls still return 200 and I do not understand why....however if I access the application inside of the box everything works as expected and I can see the 401 in the browser
Can someone maybe know a better solution for this or maybe the reason why this works locally but not in the server?
Many thanks
The solution posted in the initial question works as expected. This approach was not working in the beginning because of the custom error pages that you can define in the web.config. It was messing up with the initial solution that I found in the web.
<system.web>
<globalization enableClientBasedCulture="true" culture="en-GB" uiCulture="auto" />
<customErrors mode="Off" defaultRedirect="~/Error/Index">
<error statusCode="400" redirect="~/Error/Index" />
<error statusCode="401" redirect="~/Error/Index" />
...
...
This was redirecting to a different page and because of that was returning 200 instead of 401.
Thanks
I have the following requirements
Set a value in web.config and enable maintenance mode
All non-ajax requests should be shown a custom error page, with the http status code set to 503. The url of page should be retained.
All ajax requests should be responded with http status code 503
I should have an opportunity to do some basic logging to a file. Log the url and the user Identity if he happened to be logged into the app
I am using ELMAH to track/log all unhandled exceptions. The mechanism for implementing maintenance mode shouldn't need me to not use ELMAH
I have "runAllManagedModulesForAllRequests" set to true. this was originally done for use with RequestReduce. we no longer use it, but I am hesitant to reset its value to false. I am not sure if any other library needs it.
Once I realized there is nothing built in which supports the above requirements, I felt I had the following two options in front of me (App_offile.html won't work for me).
an HttpModule
an MVC ActionFilter
I dropped the MVC ActionFilter as I couldn't figure out how to guarantee it to run before any authentication/authorization filters. I have a custom authentication filter which hits the db. The idea behind the maintenance mode is the db might be offline, yet the web-app shouldn't show a 500 custom error page, but a 503 custom error page.
I wrote the following httpmodule and added in my web.config. It works for ajax requests. It kinda works for non-ajax requests. All requests get redirected to the 503 error page. The side-effect is all requests for static content also result in a 503. My error page thus is shown unstyled :(
// the http module
public class MaintenanceModeModule : IHttpModule
{
private static bool _isUnderMaintenance;
static MaintenanceModeModule()
{
var valueStr = (ConfigurationManager.AppSettings["UnderMaintenance"] ?? (false).ToString());
bool underMaintenance;
bool.TryParse(valueStr, out underMaintenance);
_isUnderMaintenance = underMaintenance;
}
public void Init(HttpApplication application)
{
application.BeginRequest += OnBeginRequest;
}
private void OnBeginRequest(object sender, EventArgs e)
{
var application = (HttpApplication) sender;
var request = application.Request;
var response = application.Response;
if (_isUnderMaintenance == false)
{
return;
}
application.Context.Items["under_maintenance"] = true; // used later
if (request.Url.PathAndQuery == "/503") // the url of the action that renders the custom error page
{
return;
}
const int statusCode = (int) HttpStatusCode.ServiceUnavailable;
const string statusMessage = "Temporarily down for maintenance";
var requestWrapper = new HttpRequestWrapper(request);
if (requestWrapper.IsAjaxRequest())
{
response.Clear();
response.ClearContent();
response.ClearHeaders();
response.StatusCode = statusCode;
response.TrySkipIisCustomErrors = true;
response.StatusDescription = statusMessage;
response.End();
return;
}
// doesn't work, shows the Yellow Screen of Death (YSoD)
// application.Context.Server.Transfer("~/503", preserveForm: true);
// doesn't work, shows the Yellow Screen of Death (YSoD)
// throw new HttpException(statusCode, statusMessage);
response.Redirect("~/503");
}
public void Dispose()
{
}
}
...
// web.config
// only the relevant portions of each section is shown
<appSettings>
<add key="UnderMaintenance" value="true" />
</appSettings>
<customErrors mode="On"> <!-- Custom errors are on, even then I was seeing YSoDs during my attempts -->
<error statusCode="404" redirect="404" />
<error statusCode="503" redirect="503" />
</customErrors>
<system.webServer>
<httpErrors existingResponse="PassThrough">
</httpErrors>
<modules runAllManagedModulesForAllRequests="true">
<add name="MaintenanceMode" type="WebApp.Code.MvcInfrastructure.MaintenanceModeModule" />
</modules>
</system.webServer>
...
// route config
routes.MapRoute("503Error", "503", new { controller = "Error", action = "UnderMaintenance" });
...
// error controller
// the authentication filter skips authentication if the allowanonymous attribute is present
[AllowAnonymous]
public class ErrorController : CustomBaseController
{
public ErrorController(AppConfig appConfig)
: base(appConfig)
{
}
public ActionResult UnderMaintenance()
{
// behind the scenes reads the value from HttpContext.Items.
// This was set during the execution of the httpmodule
if (AppConfig.UnderMaintenance == false)
{
return new RedirectResult("~/");
}
Response.StatusCode = (int) HttpStatusCode.ServiceUnavailable;
Response.TrySkipIisCustomErrors = true;
// the actual content of the view is not relevant now
return View("Error503");
}
}
The problems with this approach,
Each non-ajax request is responded with a 302 and then a 503
The URL requested by the browser is not retained
It returns a 503 for all static assets as well
The code I wrote and web.config settings I enabled are all cobbled together from various sources. I am not fully sure what those settings do or what the recommended way is. Please feel free to answer with a completely different method, as long as it can meet the requirements stated.
I have set up an mvc app with an _error.cshtml that is set to catch exceptions I throw in the controller.
I also have a few ajax posts on some pages that checks for errors and then it does something else.
On the server, I have a filter on all exceptions and then check if it is an ajax request and return something that can be deserialized on the client. The problem is that if I do not set the post response status code to 500 then ajax will not see this error and I can't show a nice message. If I set the status to 500 I get the default IIS error message stating something happened on the server.
I would like to handle some errors on the page in the ajax results but maintain the generic error handling. Is this an IIS setting to allow custom 500 message per site? The web.config Custom Error On|Off makes no difference in my case.
The filter you have on all exceptions that is checking if its an ajax request, is that a filter made on your own?
I had a slightly similar issue, and I had to make sure the flag TrySkipIisCustomErrors was set as true in order to avoid the standard IIS error.
This flag is located on the Response object of the HttpContext.
This is also done by the standard HandleError filter, pay attention to the last line in its implementation of the OnException method:
public virtual void OnException(ExceptionContext filterContext) {
if (filterContext == null) {
throw new ArgumentNullException("filterContext");
}
if (filterContext.IsChildAction) {
return;
}
// If custom errors are disabled, we need to let the normal ASP.NET exception handler
// execute so that the user can see useful debugging information.
if (filterContext.ExceptionHandled || !filterContext.HttpContext.IsCustomErrorEnabled) {
return;
}
Exception exception = filterContext.Exception;
// If this is not an HTTP 500 (for example, if somebody throws an HTTP 404 from an action method),
// ignore it.
if (new HttpException(null, exception).GetHttpCode() != 500) {
return;
}
if (!ExceptionType.IsInstanceOfType(exception)) {
return;
}
string controllerName = (string)filterContext.RouteData.Values["controller"];
string actionName = (string)filterContext.RouteData.Values["action"];
HandleErrorInfo model = new HandleErrorInfo(filterContext.Exception, controllerName, actionName);
filterContext.Result = new ViewResult {
ViewName = View,
MasterName = Master,
ViewData = new ViewDataDictionary<HandleErrorInfo>(model),
TempData = filterContext.Controller.TempData
};
filterContext.ExceptionHandled = true;
filterContext.HttpContext.Response.Clear();
filterContext.HttpContext.Response.StatusCode = 500;
// Certain versions of IIS will sometimes use their own error page when
// they detect a server error. Setting this property indicates that we
// want it to try to render ASP.NET MVC's error page instead.
filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
}
Here is the code for my base controller, the idea is that if the Authorization string is not in the HTTP Headers we kick them out. I swear it was working properly and now suddenly it does not work. Strangely when I debug it is actually stepping INTO the if statement so it is indeed true that the HTTP Header I am requesting is a NULL OR EMPTY string, HOWEVER, it is not exiting early and returning 403 Access Denied anymore... it was working fine and suddenly it is just ignoring the entire thing and eventually crashing later on in the app when I try to parse the Authorization String that WAS NOT ACTUALLY FOUND.
public class AuthController : Controller
{
protected int AccountID;
protected override void OnAuthorization(AuthorizationContext filterContext)
{
//if no authorization string is provided, access denied
if (string.IsNullOrEmpty(filterContext.HttpContext.Request.Headers["Authorization"]))
{
filterContext.Result = Content("Access Denied", "text/plain");
filterContext.HttpContext.Response.StatusCode = 403; //forbidden
base.OnAuthorization(filterContext);
}
//otherwise grab the authorization string and validate it
string authString = filterContext.HttpContext.Request.Headers["Authorization"];
string urlPath = string.IsNullOrEmpty(filterContext.HttpContext.Request.Path) ? "" : filterContext.HttpContext.Request.Path;
int getAccountID = 0;
//if authorization fails...
if (!AuthCore.Authorize(authString, urlPath, ref getAccountID))
{
filterContext.Result = Content("Access Denied", "text/plain");
filterContext.HttpContext.Response.StatusCode = 403; //forbidden
base.OnAuthorization(filterContext);
}
//AccountID will never be zero at this point
AccountID = getAccountID;
//carry on with Controller Action, request is valid and AccountID is known
base.OnAuthorization(filterContext);
}
UPDATE: Just tried filterContext.Result = new HttpUnauthorizedResult(); instead, same results. Controller action continues and throws error when trying to parse the header string that was not found.
UPDATE 2: Added "return;" after each of the base.OnAuthorization() calls besides the last one, now when it fails I get a 302 moved from MVC followed by a 404, which turns out is the app trying to redirect to a default login page URL that does not actually exist... could this be good enough? Maybe but I'd rather block it straight out rather than letting some wonky redirect happen as the way of blocking them, doesn't feel secure to me.
AH HA!
I was calling the base.OnAuthorization() too many times, apparently it's not actually a permanent goodbye from the thread... not sure why I thought it was now that I think about it... here is the working code:
protected override void OnAuthorization(AuthorizationContext filterContext)
{
int getAccountID = 0;
//if no authorization string is provided, access denied
if (string.IsNullOrEmpty(filterContext.HttpContext.Request.Headers["Authorization"]))
{
filterContext.Result = Content("Access Denied", "text/plain");
filterContext.HttpContext.Response.StatusCode = 403; //forbidden
}
else
{
//otherwise grab the authorization string and validate it
string authString = filterContext.HttpContext.Request.Headers["Authorization"];
string urlPath = string.IsNullOrEmpty(filterContext.HttpContext.Request.Path) ? "" : filterContext.HttpContext.Request.Path;
//if authorization fails...
if (!AuthCore.Authorize(authString, urlPath, ref getAccountID))
{
filterContext.Result = Content("Access Denied", "text/plain");
filterContext.HttpContext.Response.StatusCode = 403; //forbidden
}
}
//AccountID will never be zero at this point
AccountID = getAccountID;
//carry on with Controller Action, request is valid and AccountID is known
base.OnAuthorization(filterContext);
}
I think you should check out this post:
Securing your ASP.NET MVC 3 Application