Suppose I have the following <httpErrors>
collection in a web.config
:
<httpErrors>
</httpErrors>
Yep, nice 'n empty.
And in IIS 7, my HTTP errors page looks like this:
Beautiful! (I've highlighted 404 simply because that's the example I'll use in a sec).
Now, I run the following code:
errorElement["statusCode"] = 404;
errorElement["subStatusCode"] = -1;
errorElement["path"] = "/404.html";
httpErrorsCollection.Add(errorElement);
Lovely. I now have, as expected this in my web.config
:
<httpErrors>
<remove statusCode="404" subStatusCode="-1" />
<error statusCode="404" subStatusCode="-1" prefixLanguageFilePath="" path="/404.html" />
</httpErrors>
Couldn't be happier. Now, in IIS 7 my HTTP errors section looks, as expected, like below:
Life couldn't be sweeter at this point. Now, down the line, I want to programmatically revert my 404 error back to the state shown in the original screenshot. Logic dictates that I should remove
my new error:
httpErrorsCollection.Remove(errorElement);
But alas, if I do this, my web.config
looks a lot like this:
<httpErrors>
<remove statusCode="404" subStatusCode="-1" />
</httpErrors>
And my IIS looks a bit like this:
This is expected because of my web.config
- but how, using ServerManager
and all it's useful IIS 7 API, do I remove the httpError
element entirely and revert my web.config
back to:
<httpErrors>
</httpErrors>
Any ideas?
In IIS7 and above under Management section, we can see an icon named Configuration Editor,
double click and expand to
system.webserver-->webdav-->httpErrors
Right click on Default path, under
section-->Click on Revert to Parent
Then restart the website
The changes will be reverted back
I've bumped into this before. The only way I could make this work was to call RevertToParent()
on the system.webServer/httpErrors
section and commit the changes before making any changes, e.g.:
ConfigurationSection httpErrorsSection =
config.GetSection("system.webServer/httpErrors");
// Save a copy of the errors collection first (see pastebin example)
httpErrorsCollectionLocal =
CopyLocalErrorCollection(httpErrorsSection.GetCollection()
.Where(e => e.IsLocallyStored)
.ToList<ConfigurationElement>());
httpErrorsSection.RevertToParent();
serverManager.CommitChanges();
You have to commit after RevertToParent() is called because the errors collection remains intact until
CommitChanges()` is called.
You then have to re-add the saved copy of the local errors collection removing or updating the custom errors as they are re-added.
I've pasted a full working example below. The code works on the site root web.config
but with a bit of tinkering you could add support for web.config
files in subfolders:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Web.Administration;
namespace IIS7_HttpErrorSectionClearing
{
class Program
{
static void Main(string[] args)
{
long siteId = 60001;
// Add some local custom errors
CustomErrorManager.SetCustomError(siteId, 404, -1, ResponseMode.File, @"D:\websites\60001\www\404.html");
CustomErrorManager.SetCustomError(siteId, 404, 1, ResponseMode.File, @"D:\websites\60001\www\404-1.html");
CustomErrorManager.SetCustomError(siteId, 404, 5, ResponseMode.File, @"D:\websites\60001\www\404-5.html");
// Change existing local custom error
CustomErrorManager.SetCustomError(siteId, 404, 1, ResponseMode.ExecuteURL, "/404-5.aspx");
// Revert to inherited
CustomErrorManager.RevertCustomError(siteId, 404, 5);
CustomErrorManager.RevertCustomError(siteId, 404, -1);
CustomErrorManager.RevertCustomError(siteId, 404, 1);
}
}
public enum ResponseMode
{
File,
ExecuteURL,
Redirect
}
public static class CustomErrorManager
{
public static void RevertCustomError(long siteId, int statusCode, int subStatusCode)
{
List<Error> httpErrorsCollectionLocal = CopyLocalsAndRevertToInherited(siteId);
int index = httpErrorsCollectionLocal.FindIndex(e => e.StatusCode == statusCode && e.SubStatusCode == subStatusCode);
if(index > -1)
{
httpErrorsCollectionLocal.RemoveAt(index);
}
PersistLocalCustomErrors(siteId, httpErrorsCollectionLocal);
}
public static void SetCustomError(long siteId, long statusCode, int subStatusCode,
ResponseMode responseMode, string path)
{
SetCustomError(siteId, statusCode, subStatusCode, responseMode, path, null);
}
public static void SetCustomError(long siteId, long statusCode, int subStatusCode,
ResponseMode responseMode, string path, string prefixLanguageFilePath)
{
List<Error> httpErrorsCollectionLocal = CopyLocalsAndRevertToInherited(siteId);
AddOrUpdateError(httpErrorsCollectionLocal, statusCode, subStatusCode, responseMode, path, prefixLanguageFilePath);
PersistLocalCustomErrors(siteId, httpErrorsCollectionLocal);
}
private static void PersistLocalCustomErrors(long siteId, List<Error> httpErrorsCollectionLocal)
{
using (ServerManager serverManager = new ServerManager())
{
Site site = serverManager.Sites.Where(s => s.Id == siteId).FirstOrDefault();
Configuration config = serverManager.GetWebConfiguration(site.Name);
ConfigurationSection httpErrorsSection = config.GetSection("system.webServer/httpErrors");
ConfigurationElementCollection httpErrorsCollection = httpErrorsSection.GetCollection();
foreach (var localError in httpErrorsCollectionLocal)
{
ConfigurationElement remove = httpErrorsCollection.CreateElement("remove");
remove["statusCode"] = localError.StatusCode;
remove["subStatusCode"] = localError.SubStatusCode;
httpErrorsCollection.Add(remove);
ConfigurationElement add = httpErrorsCollection.CreateElement("error");
add["statusCode"] = localError.StatusCode;
add["subStatusCode"] = localError.SubStatusCode;
add["responseMode"] = localError.ResponseMode;
add["path"] = localError.Path;
add["prefixLanguageFilePath"] = localError.prefixLanguageFilePath;
httpErrorsCollection.Add(add);
}
serverManager.CommitChanges();
}
}
private static List<Error> CopyLocalsAndRevertToInherited(long siteId)
{
List<Error> httpErrorsCollectionLocal;
using (ServerManager serverManager = new ServerManager())
{
Site site = serverManager.Sites.Where(s => s.Id == siteId).FirstOrDefault();
Configuration config = serverManager.GetWebConfiguration(site.Name);
ConfigurationSection httpErrorsSection = config.GetSection("system.webServer/httpErrors");
ConfigurationElementCollection httpErrorsCollection = httpErrorsSection.GetCollection();
// Take a copy because existing elements can't be modified.
httpErrorsCollectionLocal = CopyLocalErrorCollection(httpErrorsSection.GetCollection()
.Where(e => e.IsLocallyStored).ToList<ConfigurationElement>());
httpErrorsSection.RevertToParent();
// Have to commit here because RevertToParent won't clear the collection until called.
serverManager.CommitChanges();
return httpErrorsCollectionLocal;
}
}
private static List<Error> CopyLocalErrorCollection(List<ConfigurationElement> collection)
{
List<Error> errors = new List<Error>();
foreach (var error in collection)
{
errors.Add(new Error()
{
StatusCode = (long)error["statusCode"],
SubStatusCode = (int)error["subStatusCode"],
ResponseMode = (ResponseMode)error["responseMode"],
Path = (string)error["path"],
prefixLanguageFilePath = (string)error["prefixLanguageFilePath"]
});
}
return errors;
}
private static void AddOrUpdateError(List<Error> collection, long statusCode, int subStatusCode,
ResponseMode responseMode, string path, string prefixLanguageFilePath)
{
// Add or update error
Error error = collection.Find(ce => ce.StatusCode == statusCode && ce.SubStatusCode == subStatusCode);
if (error == null)
{
collection.Add(new Error()
{
StatusCode = statusCode,
SubStatusCode = subStatusCode,
ResponseMode = responseMode,
Path = path,
prefixLanguageFilePath = prefixLanguageFilePath
});
}
else
{
error.ResponseMode = responseMode;
error.Path = path;
error.prefixLanguageFilePath = prefixLanguageFilePath;
}
}
private class Error
{
public long StatusCode { get; set; }
public int SubStatusCode { get; set; }
public ResponseMode ResponseMode { get; set; }
public string Path { get; set; }
public string prefixLanguageFilePath { get; set; }
}
}
}
Well this is what I've found for this issue:
ServerManager serverManager = new ServerManager();
Configuration config = serverManager.GetWebConfiguration(siteName);
ConfigurationSection httpErrorsSection = config.GetSection("system.webServer/httpErrors");
ConfigurationElementCollection httpErrorsCollection = httpErrorsSection.GetCollection();
foreach (ConfigurationElement errorEle in httpErrorsCollection) {
if (errorEle("statusCode") == statusCode && errorEle("subStatusCode") == subStatusCode) {
errorEle.Delete();
serverManager.CommitChanges();
return;
}
}
I get a list of elements as normal, then loop to find the one I want, and then call delete on the element. That's it and it works. I don't know if this existed when this question was asked, but it does now. You'll have to make sure you know that status code and substatus code.