Tag Archives: Errors and Exceptions

Health Monitoring a sledování chyb v ASP.NET webových službách (.asmx)

Jak jsem již popisoval v předchozím články, nevýhodou jinak šikovného mechanizmu Health Monitoringu je, že nesbírá chyby z aynchronních requestů a z webových služeb ASP.NET. Jak se vypořádat s prvním problémem u AJAX requestů bylo již naznačeno, podívejme se teď na problematiku webových služeb ASP.NET (.asmx). V zásadě jde opět o zachycení problémové výjimky a předání WebRequestErrorEvent.

SoapExceptionHealthMonitoringHandler (SoapExtension)

Elegantní metodou jak se dostat k odběru výjimek během zpracování webových služeb a jak je předávat mechanizmu Health Monitoringu je požití SoapExtension. Uvádím opět kód kolegy Jiřího Kandy (pomocná třída WebRequestErrorEventExt byla již definována v předchozím článku):

/// <summary>
/// V případě chyby ve zpracování web metody (webové služby) zajistí oznámení chyby health monitoringem.
/// Pozor, toto nefunguje (a chyby healthmonitoringu tak nejsou oznamovány),
/// pokud se webové služby testují v browseru!!! Pro testování nutno použít skutečného klienta webové služby (třeba service reference v konzolovce).
/// </summary>
public class SoapExceptionHealthMonitoringHandler : System.Web.Services.Protocols.SoapExtension
{
    #region ProcessMessage
    public override void ProcessMessage(System.Web.Services.Protocols.SoapMessage message)
    {
        try
        {
            if ((message != null) && (message.Stage == SoapMessageStage.AfterSerialize))
            {
                if (message.Exception != null)
                {
                    Exception exception = message.Exception;
                    if ((exception is SoapException) && (exception.InnerException != null))
                    {
                        exception = exception.InnerException;
                    }
                    if ((exception is HttpUnhandledException) && (exception.InnerException != null))
                    {
                        exception = exception.InnerException;
                    }
                    new WebRequestErrorEventExt(exception.Message, message, exception).Raise();
                }
            }
        }
        catch // pokud by zde nedejbože došlo k nějaké další výjimce, tak ji zamaskujeme
        {
            // NOOP
        }
    }
    #endregion

    #region GetInitializer
    public override object GetInitializer(Type serviceType)
    {
        return null;
    }

    public override object GetInitializer(LogicalMethodInfo methodInfo, SoapExtensionAttribute attribute)
    {
        return null;
    }
    #endregion

    #region Initialize
    public override void Initialize(object initializer)
    {
    }
    #endregion
}

…a zapojení ve web.configu potom v elementu configuration/system.web/webServices:

<webServices>
    <soapExtensionTypes>
        <add type="MyNamespace.SoapExceptionHealthMonitoringHandler, MyAssembly" priority="0" group="0" />
    </soapExtensionTypes>
</webServices>

A JE TO.

Health Monitoring a sledování chyb asynchronních postbacků (AJAX)

Health Monitoring je šikovný vestavěný mechanizmus ASP.NET pro sledování a hlášení problémových situací. Typicky je používán pro zápis výjimek aplikace do event-logu nebo jejich posílání mailem.

Slabinou Health Monitoringu je, že se neumí vypořádat s chybami vzniklými během asynchronních postbacků (AJAX) ani s chybami vzniklými v rámci webových služeb ASP.NET. Přesněji řečeno ASP.NET na tyto chyby nějak nepamatuje a neoznamuje je do web-events mechanizmu, na němž je Health Monitoring závislý.

Podívejme se nyní na cestu, jak doplnit do Health Monitoringu sledování chyb AJAXu, sledování chyb webových služeb ponechávám do samostatného článku. Jak již bylo naznačeno, jde o to, že potřebujeme doplnit chybějící oznamování chyb do web.events mechanizmu. Chyba vzniklá během asynchronního postbacku způsobí vyvolání události ScriptManager.AsyncPostBackError. Potřebujeme se tedy napojit na tuto událost s vlastní obsluhou a v ní chybu předat jako WebRequestErrorEvent.

AjaxHealthMonitoring control

Jedním z možných opakovaně použitelných elegantních řešení je vytvoření vlastního controlu, který se bude umisťovat do stránek stejně jako samotný ScriptManager, např. tedy v MasterPage. Uvádím zde kód kolegy Jirky Kandy, který se s tím vypořádal takto:

public class AjaxHealthMonitoring: Control
{
    #region OnInit
    protected override void OnInit(EventArgs e)
    {
        base.OnInit(e);

        ScriptManager scriptManager = ScriptManager.GetCurrent(Page);
        if (scriptManager == null)
        {
            throw new InvalidOperationException("Ve stránce nebyl nalezen ScriptManager, který je controlem AjaxHealthMonitoring vyžadován.");
        }
        scriptManager.AsyncPostBackError += new EventHandler<AsyncPostBackErrorEventArgs>(ScriptManager_AsyncPostBackError);
    }
    #endregion

    #region ScriptManager_AsyncPostBackError
    /// <summary>
    /// Obsluha události AsyncPostBackError ScriptManageru. Zajistí vyvolání události health monitoringu.
    /// </summary>
    private void ScriptManager_AsyncPostBackError(object sender, AsyncPostBackErrorEventArgs e)
    {
        if (e.Exception != null)
        {
            new WebRequestErrorEventExt(e.Exception.Message, this, e.Exception).Raise();
        }
    }
    #endregion
}

Pro úplnost uvádím odvozenou podobou třídy WebRequestErrorEventExt:

public class WebRequestErrorEventExt : WebRequestErrorEvent
{
    public WebRequestErrorEventExt(string message, object eventSource, Exception exception)
        : base(message, eventSource, WebEventCodes.WebExtendedBase + 999, exception)
    {
    }
}

Control se pak používá ve stránce stejně jako ScriptManager (do výstupního HTML kódu nic nerenderuje):

<asp:ScriptManager ScriptMode="Release" AllowCustomErrorsRedirect="true" runat="server" />
<havit:AjaxHealthMonitoring runat="server" />

SmtpTraceListener – mailování výstupů trace

V návaznosti na používání ExceptionTraceru, který pomocí standardní .NET mechanizmů Trace/TraceSource loguje neobsloužené výjimky, jsem dopsal jednoduchý TraceListener, který lze používání pro mailování těchto výjimek na určený mail. Cílovým scénářem je tedy automatické mailování výjimek z consolových/WinForm aplikací, či spíše utilit. Není to zatím příliš vyladěno, spíše takový náznak, jakou cestou se vydat.

Do činnosti se to zapojuje zhruba takto (app.config):

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <system.diagnostics>
        <sources>
            <source name="Exceptions" switchValue="Error">
                	<listeners>
<!--                    <add name="XmlListener" initializeData="Exceptions.xml" type="System.Diagnostics.XmlWriterTraceListener"/>
                    <add name="TextWriterListener" initializeData="Exceptions.log" type="System.Diagnostics.TextWriterTraceListener"/>
-->
                    <add name="SmtpListener" initializeData="Subject=Chyby z mojí utility;To=errors@firma.cz" type="Havit.Diagnostics.SmtpTraceListener, Havit"/>
                </listeners>
            </source>
        </sources>
    </system.diagnostics>
    <system.net>
        <mailSettings>
            <smtp deliveryMethod="Network" from="neco@nekde.cz">
                <network host="mail.server.cz"/>
            </smtp>
        </mailSettings>
    </system.net>
</configuration>

Základem je implementovat třídu dědící z předka TraceListener:

  /// <summary>
    /// TraceListener, který výstup posílá mailem.
    /// </summary>
    /// <remarks>
    /// Inspirováno implementaci System.Diagnostics.XmlWriterListener.
    /// </remarks>
    public class SmtpTraceListener : TraceListener
    {
        #region MailTo
        /// <summary>
        /// E-mailová adresa, na kterou se posílají zprávy.
        /// </summary>
        public string MailTo
        {
            get
            {
                if (_mailTo == null)
                {
                    return "devmail@havit.cz";
                }
                return _mailTo;
            }
            set
            {
                _mailTo = value;
            }
        }
        private string _mailTo;
        #endregion

        #region Subject
        /// <summary>
        /// Subject zprávy.
        /// </summary>
        public string Subject
        {
            get
            {
                if (_subject == null)
                {
                    return "SmtpTraceListener";
                }
                return _subject;
            }
            set
            {
                _subject = value;
            }
        }
        private string _subject;
        #endregion

        #region Constructors
        /// <summary>
        /// Constructor, který je volán při použití TraceListerneru z app.configu a předává se do něj hodnota atributu initializeData.
        /// </summary>
        /// <param name="initializeData">hodnota atributu initializeData z app.config</param>
        public SmtpTraceListener(string initializeData)
        {
            if (initializeData == null)
            {
                return; // použijí se defaulty                
            }

            foreach (string arg in initializeData.Split(';'))
            {
                string[] paramValue = arg.Split('=');
                if (paramValue.Length >= 2)
                {
                    switch (paramValue[0].Trim().ToUpper())
                    {
                        case "TO":
                            MailTo = paramValue[1].Trim();
                            break;
                        case "SUBJECT":
                            Subject = paramValue[1].Trim();
                            break;
                        default:
                            throw new InvalidOperationException("Neznámý parametr konfigurace SmtpTraceListeneru v initializeData.");
                    }
                }
            }
        }
        #endregion

        #region SendMessage
        /// <summary>
        /// Interní implementace odesílání mailu.
        /// </summary>
        /// <param name="message">zpráva z trace</param>
        private void SendMessage(string message)
        {
            if (String.IsNullOrEmpty(this.MailTo))
            {
                return;
            }

            try
            {
                MailMessage mailMessage = new MailMessage();
                mailMessage.To.Add(this.MailTo);
                mailMessage.Subject = this.Subject;
                mailMessage.Body = message;
                SmtpClient smtpClient = new SmtpClient();
                smtpClient.Send(mailMessage);
            }
            catch
            {
                // NOOP - nechceme, aby nám nefunkční trace-mailing zabil server
#if DEBUG
                // při debugování nás to ale zajímá
                throw;
#endif
            }

            // http://www.codeproject.com/KB/trace/smtptracelistenerarticle.aspx
            // In the SMTPTraceListener Write method - I call the Flush method. This forces the e-mail output to happen right then, and makes the component more stable.
            // With the Flush taken out of the Write method, I was experiencing some inconsistent behavior - i.e. exceptions thrown sometimes but not always...
            // goofy problem perhaps someone knows why?
            this.Flush();
        }
        #endregion

        #region SendTrace
        /// <summary>
        /// Hlavní interní implementace sestavení mailu.
        /// </summary>
        /// <param name="eventCache">A <see cref="T:System.Diagnostics.TraceEventCache"/> object that contains the current process ID, thread ID, and stack trace information.</param>
        /// <param name="source">A name used to identify the output, typically the name of the application that generated the trace event.</param>
        /// <param name="eventType">One of the <see cref="T:System.Diagnostics.TraceEventType"/> values specifying the type of event that has caused the trace.</param>
        /// <param name="id">A numeric identifier for the event.</param>
        /// <param name="data">An array of objects to emit as data. Pokud je string, obsahuje přímo text zprávy.</param>
        private void SendTrace(TraceEventCache eventCache, string source, TraceEventType eventType, int id, params object[] data)
        {
            StringBuilder message = new StringBuilder();
            foreach (object item in data)
            {
                if (item != null)
                {
                    message.AppendLine(item.ToString());
                }
            }
            message.AppendLine();

            message.Append("CommandLine: ");
            message.AppendLine(Environment.CommandLine);

            message.Append("CurrentDirectory: ");
            message.AppendLine(Environment.CurrentDirectory);

            message.Append("MachineName: ");
            message.AppendLine(Environment.MachineName);

            message.Append("UserDomainName: ");
            message.AppendLine(Environment.UserDomainName);

            message.Append(".NET Framework: ");
            message.AppendLine(Environment.Version.ToString());

            if (eventCache != null)
            {
                message.AppendLine();
                message.AppendLine("Call stack:");
                message.AppendLine(eventCache.Callstack);
                message.AppendLine();

                message.AppendLine("Logical operation stack:");
                foreach (object item in eventCache.LogicalOperationStack)
                {
                    if (item != null)
                    {
                        message.AppendLine(item.ToString());
                    }
                }
                message.AppendLine();

                message.Append("DateTime: ");
                message.AppendLine(eventCache.DateTime.ToString());

                message.Append("Timestamp: ");
                message.AppendLine(eventCache.Timestamp.ToString());

                message.Append("ProcessId: ");
                message.AppendLine(eventCache.ProcessId.ToString());

                message.Append("ThreadId: ");
                message.AppendLine(eventCache.ThreadId);
            }

            if (!String.IsNullOrEmpty(source))
            {
                message.Append("Source: ");
                message.AppendLine(source);
            }

            message.Append("EventType: ");
            message.AppendLine(eventType.ToString("g"));

            message.Append("EventId: ");
            message.AppendLine(id.ToString("g"));

            SendMessage(message.ToString());
        }
        #endregion

        #region TraceData (override)
        /// <summary>
        /// Writes trace information, an array of data objects and event information to the listener specific output.
        /// </summary>
        /// <param name="eventCache">A <see cref="T:System.Diagnostics.TraceEventCache"/> object that contains the current process ID, thread ID, and stack trace information.</param>
        /// <param name="source">A name used to identify the output, typically the name of the application that generated the trace event.</param>
        /// <param name="eventType">One of the <see cref="T:System.Diagnostics.TraceEventType"/> values specifying the type of event that has caused the trace.</param>
        /// <param name="id">A numeric identifier for the event.</param>
        /// <param name="data">An array of objects to emit as data.</param>
        /// <PermissionSet>
        ///     <IPermission class="System.Security.Permissions.EnvironmentPermission, mscorlib, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" version="1" Unrestricted="true"/>
        ///     <IPermission class="System.Security.Permissions.SecurityPermission, mscorlib, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" version="1" Flags="UnmanagedCode"/>
        /// </PermissionSet>
        public override void TraceData(TraceEventCache eventCache, string source, TraceEventType eventType, int id, params object[] data)
        {
            if ((Filter == null) || Filter.ShouldTrace(eventCache, source, eventType, id, null, null, null, data))
            {
                SendTrace(eventCache, source, eventType, id, data);
            }
        }

        /// <summary>
        /// Writes trace information, an array of data objects and event information to the listener specific output.
        /// </summary>
        /// <param name="eventCache">A <see cref="T:System.Diagnostics.TraceEventCache"/> object that contains the current process ID, thread ID, and stack trace information.</param>
        /// <param name="source">A name used to identify the output, typically the name of the application that generated the trace event.</param>
        /// <param name="eventType">One of the <see cref="T:System.Diagnostics.TraceEventType"/> values specifying the type of event that has caused the trace.</param>
        /// <param name="id">A numeric identifier for the event.</param>
        /// <param name="data">An array of objects to emit as data.</param>
        /// <PermissionSet>
        ///     <IPermission class="System.Security.Permissions.EnvironmentPermission, mscorlib, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" version="1" Unrestricted="true"/>
        ///     <IPermission class="System.Security.Permissions.SecurityPermission, mscorlib, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" version="1" Flags="UnmanagedCode"/>
        /// </PermissionSet>
        public override void TraceData(TraceEventCache eventCache, string source, TraceEventType eventType, int id, object data)
        {
            if ((Filter == null) || Filter.ShouldTrace(eventCache, source, eventType, id, null, null, data, null))
            {
                SendTrace(eventCache, source, eventType, id, data);
            }
        }
        #endregion

        #region TraceEvent (override)
        /// <summary>
        /// Writes trace information, a message, and event information to the listener specific output.
        /// </summary>
        /// <param name="eventCache">A <see cref="T:System.Diagnostics.TraceEventCache"/> object that contains the current process ID, thread ID, and stack trace information.</param>
        /// <param name="source">A name used to identify the output, typically the name of the application that generated the trace event.</param>
        /// <param name="eventType">One of the <see cref="T:System.Diagnostics.TraceEventType"/> values specifying the type of event that has caused the trace.</param>
        /// <param name="id">A numeric identifier for the event.</param>
        /// <param name="message">A message to write.</param>
        /// <PermissionSet>
        ///     <IPermission class="System.Security.Permissions.EnvironmentPermission, mscorlib, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" version="1" Unrestricted="true"/>
        ///     <IPermission class="System.Security.Permissions.SecurityPermission, mscorlib, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" version="1" Flags="UnmanagedCode"/>
        /// </PermissionSet>
        public override void TraceEvent(TraceEventCache eventCache, string source, TraceEventType eventType, int id, string message)
        {
            if ((Filter == null) || Filter.ShouldTrace(eventCache, source, eventType, id, message, null, null, null))
            {
                SendTrace(eventCache, source, eventType, id, message);
            }
        }

        /// <summary>
        /// Writes trace information, a formatted array of objects and event information to the listener specific output.
        /// </summary>
        /// <param name="eventCache">A <see cref="T:System.Diagnostics.TraceEventCache"/> object that contains the current process ID, thread ID, and stack trace information.</param>
        /// <param name="source">A name used to identify the output, typically the name of the application that generated the trace event.</param>
        /// <param name="eventType">One of the <see cref="T:System.Diagnostics.TraceEventType"/> values specifying the type of event that has caused the trace.</param>
        /// <param name="id">A numeric identifier for the event.</param>
        /// <param name="format">A format string that contains zero or more format items, which correspond to objects in the <paramref name="args"/> array.</param>
        /// <param name="args">An object array containing zero or more objects to format.</param>
        /// <PermissionSet>
        ///     <IPermission class="System.Security.Permissions.EnvironmentPermission, mscorlib, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" version="1" Unrestricted="true"/>
        ///     <IPermission class="System.Security.Permissions.SecurityPermission, mscorlib, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" version="1" Flags="UnmanagedCode"/>
        /// </PermissionSet>
        public override void TraceEvent(TraceEventCache eventCache, string source, TraceEventType eventType, int id, string format, params object[] args)
        {
            if ((Filter == null) || Filter.ShouldTrace(eventCache, source, eventType, id, format, args, null, null))
            {
                SendTrace(eventCache, source, eventType, id, String.Format(CultureInfo.InvariantCulture, format, args));
            }
        }
        #endregion

        #region TraceTransfer (override)
        /// <summary>
        /// Writes trace information, a message, a related activity identity and event information to the listener specific output.
        /// </summary>
        /// <param name="eventCache">A <see cref="T:System.Diagnostics.TraceEventCache"/> object that contains the current process ID, thread ID, and stack trace information.</param>
        /// <param name="source">A name used to identify the output, typically the name of the application that generated the trace event.</param>
        /// <param name="id">A numeric identifier for the event.</param>
        /// <param name="message">A message to write.</param>
        /// <param name="relatedActivityId">A <see cref="T:System.Guid"/> object identifying a related activity.</param>
        public override void TraceTransfer(TraceEventCache eventCache, string source, int id, string message, Guid relatedActivityId)
        {
            SendTrace(eventCache, source, TraceEventType.Transfer, id, String.Format("{0} : {1}", message, relatedActivityId));
        }
        #endregion

        #region Write, WriteLine (override)
        /// <summary>
        /// When overridden in a derived class, writes the specified message to the listener you create in the derived class.
        /// </summary>
        /// <param name="message">A message to write.</param>
        public override void Write(string message)
        {
            TraceEvent(null, "Write", TraceEventType.Information, 0, message);
        }

        /// <summary>
        /// When overridden in a derived class, writes a message to the listener you create in the derived class, followed by a line terminator.
        /// </summary>
        /// <param name="message">A message to write.</param>
        public override void WriteLine(string message)
        {
            this.Write(message);
        }
        #endregion
    }

WebResource.axd: This is an invalid webresource request. Špatné datum na serveru.

Symptom

Všechny requesty na WebResource.axd (včetně základních na .NET skripty) končí chybou 404 – „This is an invalid webresource request.“, resp. „Toto je neplatný požadavek webového prostředku.“

Cause

Problém byl v tom, že na serveru byl nastaven čas o několik měsíců nazpět.

Jestli tomu vadí, že aplikace byla kompilována později, než je datum/čas na serveru, nebo co konkrétně je problém, to jsem nehledal. Každopádně navrácení korektního data okamžitě problém vyřešilo.

Bezpečnostní chyba v ASP.NET a workaround s customErrors k jejímu vyřešení

V ASP.NET byla objevena (a zveřejněna!) bezpečnostní chyba, která sofistikovanějšímu útočníkovi umožní získat obsah zdrojových souborů uložených na webovém serveru, včetně web.configu. Podrobnosti popisuje ve svém blog-postu Scott Guthrie. Workaround spočívá v zapnutí customErrors bez rozlišování typu chyby.

Kdo by to chtěl vidět na vlastní oči, tak na YouTube je pěkné video.

„Could not load file or assembly System.Web.Extensions“ při načítání controlů metodou LoadControl

Na zajímavý problém jsem narazil při implementaci použití CMS a našeho prezentačního frameworku. Obojí je kompilováno pro .NET 2.0, používáme je s .NET Frameworkem 3.5.
CMS načítá *.ascx standardní metodou:

Page.LoadControl("~/Test.ascx");

Bohužel se objevil problém s načítáním assembly, které jsou ve web.configu redirectovány na nové verze:

 

Could not load file or assembly 'System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' or one of its dependencies. Systém nemůže nalézt uvedený soubor. 
<runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
        <dependentAssembly>
            <assemblyIdentity name="System.Web.Extensions" publicKeyToken="31bf3856ad364e35"/>
            <bindingRedirect oldVersion="1.0.0.0-1.1.0.0" newVersion="3.5.0.0"/>
        </dependentAssembly>
        <dependentAssembly>
            <assemblyIdentity name="System.Web.Extensions.Design" publicKeyToken="31bf3856ad364e35"/>
            <bindingRedirect oldVersion="1.0.0.0-1.1.0.0" newVersion="3.5.0.0"/>
        </dependentAssembly>
    </assemblyBinding>
</runtime>

Příčina a řešení problému byla taková, že jsem v konfiguračním souboru musel odstranit namespace (atribut xmlns) z elementu configuration.

<configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">

Pozor na ImageUrl, resp. UrlPathEncode() a znaky % (procenta) nebo # (hash)

Při bezstarostném používání Image.ImageUrl jsem narazil na zajímavý problém – pokud se Vám v názvu souboru nebo v cestě k němu vyskytne % (procenta) nebo # (hash), pak máte problém – metoda HttpUtility.UrlPathEncode(), kterou Image.ImageUrl a mnohé další controly pro encodování URL odkazů používají, Vám tyto znaky neencoduje:

<asp:Image ImageUrl="~/Folder #3/File.jpg" runat="server" />

udělá

<img src="/Folder%20#3/File.jpg" />

protože

HttpUtility.UrlPathEncode("/Folder #3/File.jpg") == "/Folder%20#3/File.jpg"

Metoda UrlPathEncode() totiž encoduje jen mezerník a non-ASCII znaky.

Správné samozřejmě je, abyste se na webových serverech pokusili těmto znakům vyhnout. Nicméně uživatelé jsou tvořiví a tak se můžete dočkat překvapení stejně jako já.

Osobně si myslím, že se jedná o bug v metodě UrlPathEncode(), resp. o nenaplnění očekávaného contractu, nicméně Microsoft to samozřejmě interpretuje jako by-design a radí, ať si uděláte Replace() těchto znaků sami.

Nezbývá tedy než:

myImage.ImageUrl = path.Replace("%", "%25").Replace("#", "%23");

Mimochodem obdobných vychytávek se můžete dočkat i se znakem + (plus).