Stefan Holm Olsen

A nice handling of APIs when Episerver is in readonly mode

In some specific situations, an Episerver site and its database can be put in a completely read-only mode. A while back, Arild Henrichsen wrote about when this can happen and what is turned off while this lasts.

When the site is read-only, our Episerver sites should be prepared code-wise for this. Otherwise errors may happen and the user experience will degrade.

In this blog post, I describe a filter attribute for both MVC and WebAPI pipelines, which will gracefully handle the read-only experience.

Prepare for the "rainy day"

To be prepared for a situation where the whole site is in read only mode, think about all the places in your code where you change something in the database. Some of these can be:

  • Adding a variant to a cart.
  • Removing a variant from a cart.
  • Logging in, where a security stamp is updated or a refresh token is deleted.
  • Initiating a payment.
  • Switching currency, and storing it as a preferred currency on the customer profile.

When the website database is in read-only mode, all MVC and WebAPI actions that can change anything in the databases, should be temporarily disabled. But it would have to be done in a way that does not break any client, neither app or web.

How should responses look

To be semantically correct, the HTTP status code that comes closest would be “503 Service Unavailable”. By returning this code, both apps, JavaScript modules and proxy servers (like Cloudflare) will understand that this is a temporary issue. When this response comes to any of the clients, they should handle the situation politely, for instance showing an apologizing message or make minor changes in a local cache and retry later.

In Episerver's sample site, Quicksilver, there is a similar attribute for MVC actions only. But that one will return a "404 Not Found" response, which is a semantically wrong message to send for a temporary outage of an API.

Code samples

The following class can be applied to any API controller class, or specific methods. It can even be added to globally, through the HttpConfiguration class.

using System;
using System.Net;
using System.Net.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
using EPiServer.Data;
using EPiServer.ServiceLocation;

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AllowDBWriteApiAttribute : ActionFilterAttribute
{
    protected Injected<IDatabaseMode> DbMode;

    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        bool isReadOnly = DbMode.Service != null && DbMode.Service.DatabaseMode == DatabaseMode.ReadOnly;
        if (!isReadOnly)
        {
            return;
        }

        actionContext.Response = new HttpResponseMessage
        {
            StatusCode = HttpStatusCode.ServiceUnavailable,
            ReasonPhrase = "Service is read-only"
        };
    }
}

This other class can be applied to any MVC controller class, or any of their specific methods. It can also be applied globally, through the GlobalFilters class.

using System;   
using System.Net;
using System.Web.Mvc;
using EPiServer.Data;
using EPiServer.ServiceLocation;

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AllowDBWriteMvcAttribute : ActionFilterAttribute
{
    protected Injected<IDatabaseMode> DbMode;

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        bool isReadOnly = DbMode.Service != null && DbMode.Service.DatabaseMode == DatabaseMode.ReadOnly;
        if (!isReadOnly)
        {
            return;
        }

        filterContext.Result = new HttpStatusCodeResult(
            HttpStatusCode.ServiceUnavailable,
            "Service is read-only");
    }
}

Closing remarks

Besides validating if the database mode will support the MVC and WebAPI requests, we should also be able to disable those actions completely from the user interface. For instance:

  • Disable or remove the "add to cart" button.
  • Disable the login panel.
  • Disable the "go to payment" button.
  • When an app receives the 503 response, it could disable similar buttons and panels and call a ping API endpoint regularly.

Such a ping-endpoint could return the "503 Service Unavailable" response if the database is still read-only, and "204 No Content" if the database has switched back to read-write mode.