When building a .NET web application, it's common to use the combination of UseExceptionHandler
and UseStatusCodePagesWithReExecute
in your Startup.cs
to serve user-friendly error pages:
app.UseExceptionHandler("/errorhandler/500");
app.UseStatusCodePagesWithReExecute("/errorhandler/{0}");
This works seamlessly—even for Optimizely CMS solutions. However, we recently uncovered a caveat when working on a project involving custom API endpoints. It turns out Optimizely has its own hidden behavior regarding error handling, and it can silently interfere with your carefully configured pipeline.
We had an API route that intentionally returns raw HTTP status codes (e.g., 401 Unauthorized) depending on business logic. The expected behavior was to return the actual HTTP response code, not a friendly HTML error page.
However, once UseStatusCodePagesWithReExecute
is enabled, even API endpoints like /my/custom/api
get intercepted, and you’ll end up with an HTML error response when your API returns a 401.
So, we added conditional logic:
app.UseWhen(
context => !context.Request.Path.StartsWithSegments("/my/custom/api"),
appBuilder =>
{
appBuilder.UseExceptionHandler("/errorhandler/500");
appBuilder.UseStatusCodePagesWithReExecute("/errorhandler/{0}");
});
This works—until you go to production.
To our surprise, our API was still returning Optimizely's custom error page in production. After some digging, we discovered that Optimizely CMS automatically registers its own exception and status code handling, regardless of what you configure.
This happens when you call .AddCms()
—specifically, .AddCmsUI()
internally registers the following service:
services.AddStartupFilter<ErrorsStartupFilter>();
This filter looks like this:
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> nextAction)
{
return app =>
{
if (app.ApplicationServices.GetRequiredService<IWebHostEnvironment>().IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseStatusCodePagesWithReExecute("/Util/Errors/Error{0}");
app.UseExceptionHandler("/Util/Errors/Error500");
}
nextAction(app);
};
}
So, Optimizely always wires in its own error handlers—after yours.
There’s no public configuration to disable this behavior. The class is internal
, so you can’t override or configure it. We had to remove it from the service collection ourselves:
var errorStartupFilterServiceDescriptor = services
.FirstOrDefault(descriptor => descriptor.ImplementationType?.Name == "ErrorsStartupFilter");
if (errorStartupFilterServiceDescriptor != null)
{
services.Remove(errorStartupFilterServiceDescriptor);
}
By doing this early in ConfigureServices
, you prevent Optimizely’s error handlers from being injected, giving you full control over the pipeline again.
This is one of those "framework magic vs. developer control" situations. It’s easy to miss because everything seems to work locally—until production exposes the conflict.
Hopefully, this saves someone a few hours of debugging!