Pourquoi une tolérance aux fautes dans Application_Start() ?

Ce que laisse entendre le titre n’est pas tout à fait exact : la méthode Application_Start n’est pas tolérante aux fautes : une exception non capturée arrêtera son exécution. En revanche, l’application web est bien tolérante aux exceptions propagées par cette méthode.


Je me souviens avoir été un peu surpris lorsque j’ai appris cela (et je serai curieux de savoir si ma réaction est partagée par d’autres développeurs ASP.NET). Typiquement, une application lourde (console, winforms…) crashera si une exception est propagée durant son initialisation (dans son point d’entrée Program.Main() par exemple). Pourquoi autoriser une application web à fonctionner alors que son initialisation a échouée ? Cela mène typiquement à un état incertain de l’application.

Je suppose qu’il s’agit d’un choix de l’équipe de Microsoft ASP.NET pour donner plus de liberté aux implémentations de divers sites: on peut souhaiter garantir que les ressources statiques (fichiers HTML, CSS, images…) soient toujours servies méme si l’application est défaillante. Malgré tout, je ne vois aucun cas dans mon expérience où cela a été souhaitable. Si on utilise cette méthode pour l’initialisation de l’application web, on assume un certain déterminisme comme pour n’importe quelle application. Si, au moment de son initialisation, quelque chose échoue, il est préférable d’empêcher l’application de fonctionner dans un état incertain.

Rappelons que la méthode Application_Start est spéciale car invoquée au traitement de la première requête de l’application ASP.NET (cf. MSDN). Cette méthode n’est appelée qu’une seule fois pendant toute la durée de vie de l’application (son recyclage éventuel marquant sa fin de vie). Depuis cette méthode, la contexte web (HttpContext) n’est pas encore mis en place.
Par opposition, la méthode Init() est invoquée à chaque création d’instance de HttpApplication (plusieurs instances sont gérées dans un pool par ASP.NET).

Durant mes projets, j’ai pris pour habitude de toujours dériver d’une classe abstraite pour contrôler la bonne initialisation de nos applications :


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;

    public abstract class ApplicationBase : System.Web.HttpApplication
    {
        private static volatile bool _appStarted;

        private static Exception _applicationException;

        private static readonly object _appStartSync = new object();

        private readonly string _fatalErrorRedirection;

        private readonly string[] _fatalErrorResources;

        public ApplicationBase(string fatalErrorUrl, params string[] resourcesUrl)
        {
            _fatalErrorRedirection = fatalErrorUrl;
            _fatalErrorResources = resourcesUrl ?? new string[0];
        }
        
        protected abstract void LogCriticalException(Exception e);

        protected virtual void InitializeApplication()
        {
            System.Diagnostics.Debug.WriteLine("ApplicationBase.InitializeApplication()");
        }
        
        protected void Application_Start()
        {
            Monitor.Enter(_appStartSync);
            try
            {
                System.Diagnostics.Debug.WriteLine("ApplicationBase.Application_Start()...");
                InitializeApplication();
                _appStarted = true;
                System.Diagnostics.Debug.WriteLine("ApplicationBase.Application_Start(): initialisation réussie.");
            }
            catch (Exception ex)
            {
                System.Diagnostics.Debug.WriteLine(ex.ToString());
                _applicationException = new ApplicationException("Une exception s'est produite dans la méthode Application_Start. Consultez les traces. L'application étant dans un état instable, toutes les requêtes sont interrompues.");
                LogCriticalException(ex);
                throw;
            }
            finally
            {
                Monitor.Exit(_appStartSync);
            }
        }

        protected void Application_BeginRequest()
        {
            if (_appStarted)
                return;

            #region L'application n'a pas été correctement initialisée: empeche de servir toute ressource autre que celles définies dans le constructeur.
            var context = HttpContext.Current;
            var rawUrl = context.Request.RawUrl;
            bool isFatalErrorPage = _fatalErrorRedirection != null && rawUrl.StartsWith(_fatalErrorRedirection, StringComparison.OrdinalIgnoreCase);
            if (!isFatalErrorPage
                && _fatalErrorResources.Where(t => t != null && rawUrl.StartsWith(t, StringComparison.OrdinalIgnoreCase)).Count() == 0)
            {
                const int timeoutMs = 10 * 1000;
                if (Monitor.TryEnter(_appStartSync, timeoutMs))
                {
                    try
                    {
                        if (!_appStarted)
                        {
                            const bool endResponse = true;
                            LogCriticalException(_applicationException);
                            if (_fatalErrorRedirection == null)
                            {
                                // Retourne une erreur 500 sans contenu
                                Context.Response.TrySkipIisCustomErrors = true;
                                Response.Status = "500 ServerError";
                                Response.StatusCode = 500;
                                Response.StatusDescription = "Application initialization error";
                                Response.ClearContent();
                                Response.End();
                            }

                            // Redirection vers la page d'erreur critique (obtiendra un statut 500 également)
                            context.Response.Redirect(_fatalErrorRedirection, endResponse);
                        }
                    }
                    finally
                    {
                        Monitor.Exit(_appStartSync);
                    }
                }
                else
                {
                    // Retourne manuellement une réponse 503
                    Context.Response.TrySkipIisCustomErrors = true;
                    Response.ClearHeaders();
                    Response.ClearContent();
                    Response.Status = "503 ServiceUnavailable";
                    Response.StatusCode = 503;
                    Response.StatusDescription = "Service temporary unavailable";
                    Response.End();
                }
            }
            else if (isFatalErrorPage)
            {
                // Force un statut 500
                var filepath = Context.Server.MapPath(_fatalErrorRedirection);
                Context.Response.TrySkipIisCustomErrors = true;
                Response.Status = "500 ServerError";
                Response.StatusCode = 500;
                Response.StatusDescription = "Application initialization error";
                Response.ClearContent();
                Response.WriteFile(filepath);
                Response.End();
            }
            #endregion
        }
    }

Le code fourni ci-dessus n’ayant été testé qu’en mode intégré (IIS 7+), il est probable que la gestion du statut d’erreur HTTP, sous cette forme, ne fonctionne pas en mode classique.

La classe ApplicationBase ci-dessus est très simple. La méthode Application_Start invoque la méthode abstraite InitializeApplication. La logique d’initialisation habituellement placée dans la première doit l’être dans cette dernière. À la fin de l’initialisation, si aucune exception ne s’est produite, un flag est activé. Celui-ci est vérifié à chaque nouvelle requête. Les exceptions sont transmises à la méthode abstraite LogCriticalException que l’on implémentera typiquement avec ELMAH (exemple ci-dessous). Il faut conserver à l’esprit que le contexte web n’est pas disponible lors de l’initialisation de l’application.

En cas d’erreur, une page d’erreur (fichier statique) avec un code de statut HTTP 500 est retourné pour toutes les requêtes reçues tant que l’application n’est pas redémarrée (et l’origine de l’erreur résorbée).

Voici un exemple d’utilisation:


    public class MvcApplication : ApplicationBase
    {
        public MvcApplication()
            : base("/_app_offline.htm", "/elmah.axd")
        {

        }

        protected override void InitializeApplication()
        {
     // Initialisation (conteneur IOC, configuration MVC, etc.)
     // [...]
        }

        protected override void LogCriticalException(Exception e)
        {
            Elmah.ErrorLog.GetDefault(null).Log(new Elmah.Error(e));
        }
    }

En cas d’erreur, seule la page « /_app_offline.htm » pourra être servie, ainsi que toute URL commençant par « /elmah.axd« . Si aucune URL n’est fournie au constructeur de base, seule une réponse 500 sera retournée (sans contenu).
Si la méthode Application_BeginRequest() doit être surchargée, il est important d’appeler son implémentation de base en premier lieu.