14
votes

I've developed a sample SignalR application based on ASP.NET 4.5 & Owin, and I've hosted that app on IIS 7.5.

Everything is working fine, but how can I handle exceptions in Owin?

Consider the following code:

[HubName("SampleHub")]
public class SampleHub : Hub
{
    public SampleHub()
    {
        throw new InvalidOperationException("?!");
    }
}

This exception won't call Application_Error (and this is my problem).

Where can I get all exceptions from Owin for logging and debugging purposes similarly to Application_Error?

I'm not interested in something like this:

app.UseErrorPage(new ErrorPageOptions()
{
    ShowCookies = true,
    ShowEnvironment = true,
    ShowExceptionDetails = true,
    ShowHeaders = true,
    ShowQuery = true,
    ShowSourceCode = true
});

This is totally useless for advanced scenarios, something like ASP.NET Web API and ASP.NET MVC.

Action filters with OnException method for override purposes is much better.

2

2 Answers

23
votes

If you want exception handling specifically for SignalR Hubs, OWIN middleware is not the way to go.

To illustrate just one reason why, suppose that SignalR is using its WebSocket transport when an exception is thrown from inside a Hub method. In this case, SignalR will not close the WebSocket connection. Instead SignalR will write a JSON encoded message directly to the socket to indicate to the client that an exception was thrown. There is no easy way using OWIN middleware to trigger any sort of event when this happens outside of possibly wrapping the entire OWIN WebSocket Extension which I would strongly advise against.

Fortunately SignalR provides its own Hub Pipeline which is perfectly suited for your scenario.

using System;
using System.Diagnostics;
using Microsoft.AspNet.SignalR.Hubs;

public class MyErrorModule : HubPipelineModule
{
    protected override void OnIncomingError(ExceptionContext exceptionContext, IHubIncomingInvokerContext invokerContext)
    {
        MethodDescriptor method = invokerContext.MethodDescriptor;

        Debug.WriteLine("{0}.{1}({2}) threw the following uncaught exception: {3}",
            method.Hub.Name,
            method.Name,
            String.Join(", ", invokerContext.Args),
            exceptionContext.Error);
    }
}

You can use the ExceptionContext for more than just logging. For example you can set ExceptionContext.Error to a different exception which will change the exception the client receives.

You can even suppress the exception by setting ExceptionContext.Error to null or by setting ExceptonContext.Result. If you do this, It will appear to the client that the Hub method returned the value you found in ExceptonContext.Result instead of throwing.

A while back a wrote another SO answer about how you can call a single client callback for every exception thrown by a Hub method: SignalR exception logging?

There is also MSDN documentation for HubPipelineModules: http://msdn.microsoft.com/en-us/library/microsoft.aspnet.signalr.hubs.hubpipelinemodule(v=vs.118).aspx

0
votes

The answer by @halter73 is great for errors thrown inside hubs, but it doesn't catch errors thrown during their creation.

I was getting the exception:

System.InvalidOperationException: 'foobarhub' Hub could not be resolved.

The server was returning an HTML page for this exception, but I needed it in JSON format for better integration with my Angular app, so based on this answer I implemented an OwinMiddleware to catch exceptions and change the output format. You could use this for logging errors instead.

public class GlobalExceptionMiddleware : OwinMiddleware
{
    public GlobalExceptionMiddleware(OwinMiddleware next)
        : base(next)
    {
    }

    public override async Task Invoke(IOwinContext context)
    {
        try
        {
            await Next.Invoke(context);
        }
        catch (Exception ex)
        {
            context.Response.ContentType = "application/json";
            context.Response.StatusCode = 500;

            await context.Response.WriteAsync(JsonConvert.SerializeObject(ex));
        }
    }

}

Add the registration in OwinStartup.cs, just remember to place it before the MapSignalR method call:

public class OwinStartup
{
    public void Configuration(IAppBuilder app)
    {
        app.Use<GlobalExceptionMiddleware>(); // must come before MapSignalR()

        app.MapSignalR();
    }
}