Skip to content

Commit

Permalink
Merge pull request #585 from SignalR/Issue#515
Browse files Browse the repository at this point in the history
Added custom SignalR error handling capability - Fixed #515
  • Loading branch information
davidfowl committed Aug 14, 2012
2 parents 59de15e + c414a16 commit aea3e61
Show file tree
Hide file tree
Showing 7 changed files with 199 additions and 11 deletions.
6 changes: 6 additions & 0 deletions SignalR.Client.Net35/SignalR.Client.Net35.csproj
Expand Up @@ -109,6 +109,12 @@
<Compile Include="..\SignalR.Client\Infrastructure\DisposableAction.cs">
<Link>Infrastructure\DisposableAction.cs</Link>
</Compile>
<Compile Include="..\SignalR.Client\Infrastructure\ErrorExtensions.cs">
<Link>Infrastructure\ErrorExtensions.cs</Link>
</Compile>
<Compile Include="..\SignalR.Client\Infrastructure\SignalRError.cs">
<Link>Infrastructure\SignalRError.cs</Link>
</Compile>
<Compile Include="..\SignalR.Client\Infrastructure\ThreadSafeInvoker.cs">
<Link>Infrastructure\ThreadSafeInvoker.cs</Link>
</Compile>
Expand Down
2 changes: 1 addition & 1 deletion SignalR.Client/Connection.cs
Expand Up @@ -229,7 +229,7 @@ private Task Negotiate(IClientTransport transport)
if (task.IsFaulted)
{
Stop();
tcs.SetException(task.Exception);
tcs.SetException(task.Exception.Unwrap());
}
else if (task.IsCanceled)
{
Expand Down
72 changes: 72 additions & 0 deletions SignalR.Client/Infrastructure/ErrorExtensions.cs
@@ -0,0 +1,72 @@
using System;
using System.IO;
using System.Net;

namespace SignalR.Client
{
public static class ErrorExtensions
{
/// <summary>
/// Simplifies error recognition by unwrapping complex exceptions.
/// </summary>
/// <param name="ex">The thrown exception.</param>
/// <returns>An unwrapped exception in the form of a SignalRError.</returns>
public static SignalRError GetError(this Exception ex)
{
ex = ex.Unwrap();
var wex = ex as WebException;

var error = new SignalRError(ex);

if (wex != null && wex.Response != null)
{
var response = wex.Response as HttpWebResponse;
if (response != null)
{
error.SetResponse(response);
error.StatusCode = response.StatusCode;
Stream originStream = response.GetResponseStream();

if (originStream.CanRead)
{
// We need to copy the stream over and not consume it all on "ReadToEnd". If we consumed the entire stream GetError
// would only be able to be called once per Exception, otherwise you get inconsistent ResponseBody results.
Stream stream = Clone(originStream);

// Consume our copied stream
using (var sr = new StreamReader(stream))
{
error.ResponseBody = sr.ReadToEnd();
}
}
}
}

return error;
}

private static Stream Clone(Stream source)
{
var cloned = new MemoryStream();
byte[] buffer = new byte[2048];// Copy up to 2048 bytes at a time
int copiedBytes;// Maintains how many bytes were read

// Read bytes and copy them into a buffer making sure not to trigger the dispose
while ((copiedBytes = source.Read(buffer, 0, buffer.Length)) > 0)
{
// Write the copied bytes from the buffer into the cloned stream
cloned.Write(buffer, 0, copiedBytes);
}

// Move the stream pointers back to the original start locations
if (source.CanSeek)
{
source.Seek(0, 0);
}

cloned.Seek(0, 0);

return cloned;
}
}
}
66 changes: 66 additions & 0 deletions SignalR.Client/Infrastructure/SignalRError.cs
@@ -0,0 +1,66 @@
using System;
using System.Net;

namespace SignalR.Client
{
/// <summary>
/// Represents errors that are thrown by the SignalR client
/// </summary>
public class SignalRError : IDisposable
{
private HttpWebResponse _response;

/// <summary>
/// Create custom SignalR based error.
/// </summary>
/// <param name="exception">The exception to unwrap</param>
public SignalRError(Exception exception)
{
Exception = exception;
}

/// <summary>
/// Capture the response so we can dispose of it later
/// </summary>
/// <param name="response">The HttpWebResponse associated with the Exception, assuming its a WebException</param>
public void SetResponse(HttpWebResponse response)
{
_response = response;
}

/// <summary>
/// The status code of the error (if it was a WebException)
/// </summary>
public HttpStatusCode StatusCode { get; set; }

/// <summary>
/// The response body of the error, if it was a WebException and the response is readable
/// </summary>
public string ResponseBody { get; set; }

/// <summary>
/// The unwrapped underlying exception
/// </summary>
public Exception Exception { get; private set; }

/// <summary>
/// Allow a SignalRError to be directly written to an output stream
/// </summary>
/// <returns>Exception error</returns>
public override string ToString()
{
return Exception.ToString();
}

/// <summary>
/// Dispose of the response
/// </summary>
public void Dispose()
{
if (_response != null)
{
_response.Close();
}
}
}
}
2 changes: 2 additions & 0 deletions SignalR.Client/SignalR.Client.csproj
Expand Up @@ -66,6 +66,8 @@
<Compile Include="Hubs\IHubProxy.cs" />
<Compile Include="Hubs\Subscription.cs" />
<Compile Include="IConnection.cs" />
<Compile Include="Infrastructure\ErrorExtensions.cs" />
<Compile Include="Infrastructure\SignalRError.cs" />
<Compile Include="Infrastructure\ExceptionHelper.cs" />
<Compile Include="Infrastructure\ThreadSafeInvoker.cs" />
<Compile Include="Transports\ServerSentEvents\ChunkBuffer.cs" />
Expand Down
40 changes: 37 additions & 3 deletions SignalR.Tests/ConnectionFacts.cs
@@ -1,7 +1,9 @@
using System;
using System.Threading;
using Moq;
using Moq;
using SignalR.Client.Transports;
using SignalR.Hosting.Memory;
using System;
using System.Net;
using System.Threading.Tasks;
using Xunit;

namespace SignalR.Client.Tests
Expand Down Expand Up @@ -62,6 +64,38 @@ public void FailedStartShouldNotBeActive()
Assert.Equal("Something failed.", ex.Message);
Assert.Equal(ConnectionState.Disconnected, connection.State);
}

[Fact]
public void ThrownWebExceptionShouldBeUnwrapped()
{
var host = new MemoryHost();
host.MapConnection<MyBadConnection>("/ErrorsAreFun");

var connection = new Client.Connection("http://test/ErrorsAreFun");

// Expecting 404
var aggEx = Assert.Throws<AggregateException>(() => connection.Start(host).Wait());

connection.Stop();

using (var ser = aggEx.GetError())
{
Assert.Equal(ser.StatusCode, HttpStatusCode.NotFound);
Assert.NotNull(ser.ResponseBody);
Assert.NotNull(ser.Exception);
}
}

public class MyBadConnection : PersistentConnection
{
protected override Task OnConnectedAsync(IRequest request, string connectionId)
{
// Should throw 404
using (HttpWebRequest.Create("http://www.microsoft.com/mairyhadalittlelambbut_shelikedhertwinkling_littlestar_better").GetResponse()) { }

return base.OnConnectedAsync(request, connectionId);
}
}
}
}
}
22 changes: 15 additions & 7 deletions samples/SignalR.Client.Samples/Program.cs
@@ -1,11 +1,10 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using SignalR.Client.Hubs;
#if !NET35
using SignalR.Hosting.Memory;
#endif
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;

namespace SignalR.Client.Samples
{
Expand Down Expand Up @@ -85,7 +84,10 @@ private static void RunDemoHub(HubConnection hubConnection)

demo.Invoke("multipleCalls").ContinueWith(task =>
{
Console.WriteLine(task.Exception);
using (var error = task.Exception.GetError())
{
Console.WriteLine(error);
}
}, TaskContinuationOptions.OnlyOnFaulted);

Expand Down Expand Up @@ -118,7 +120,10 @@ private static void RunStreamingSample()
connection.Error += e =>
{
Console.Error.WriteLine("========ERROR==========");
Console.Error.WriteLine(e.GetBaseException());
using (var error = e.GetError())
{
Console.Error.WriteLine(error);
}
Console.Error.WriteLine("=======================");
};

Expand Down Expand Up @@ -154,10 +159,13 @@ private static void RunStreamingSample()
{
task.Wait();
}
catch(Exception ex)
catch (Exception ex)
{
Console.Error.WriteLine("========ERROR==========");
Console.Error.WriteLine(ex.GetBaseException());
using (var error = ex.GetError())
{
Console.Error.WriteLine(error);
}
Console.Error.WriteLine("=======================");
return;
}
Expand Down

0 comments on commit aea3e61

Please sign in to comment.