Disable Optimizely CMS Default Error Handling in ASP.NET Core

Posted on Friday, July 25, 2025

Introduction

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.

 

The Problem: Custom Errors for an API Segment

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.

The Surprise: Optimizely Hooks in Its Own Middleware

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.

Our Solution: Remove the Startup Filter

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.

Final Thoughts

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!