Skip to content
This repository was archived by the owner on Sep 3, 2024. It is now read-only.

Commit 77e2e0e

Browse files
handling exceptions more generically
1 parent b83dbcf commit 77e2e0e

9 files changed

Lines changed: 207 additions & 47 deletions

File tree

src/SqlStreamStore.HAL.DevServer/Program.cs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@
77
using KestrelPureOwin;
88
using Microsoft.AspNetCore.Server.Kestrel.Core;
99
using SqlStreamStore.Streams;
10+
using MidFunc = System.Func<
11+
System.Func<
12+
System.Collections.Generic.IDictionary<string, object>,
13+
System.Threading.Tasks.Task>,
14+
System.Func<
15+
System.Collections.Generic.IDictionary<string, object>,
16+
System.Threading.Tasks.Task>>;
1017
using BuildFunc = System.Action<
1118
System.Func<
1219
System.Func<
@@ -39,7 +46,17 @@ public static async Task<int> Main(string[] args)
3946
}
4047

4148
private static Action<BuildFunc> Configure(IStreamStore streamStore)
42-
=> builder => builder.Use(SqlStreamStoreHalMiddleware.UseSqlStreamStoreHal(streamStore));
49+
=> builder => builder
50+
.Use(DisplayErrors)
51+
.Use(SqlStreamStoreHalMiddleware.UseSqlStreamStoreHal(streamStore));
52+
53+
private static MidFunc DisplayErrors => next => env => next(env).ContinueWith(_ =>
54+
{
55+
Console.WriteLine(_.Exception);
56+
57+
return Task.CompletedTask;
58+
},
59+
TaskContinuationOptions.OnlyOnFaulted);
4360

4461
private static void DisplayMenu(IStreamStore streamStore)
4562
{

src/SqlStreamStore.HAL.Tests/StreamAppendTests.cs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
{
33
using System;
44
using System.Collections.Generic;
5+
using System.Linq;
56
using System.Net;
67
using System.Net.Http;
78
using System.Net.Http.Headers;
@@ -193,6 +194,70 @@ public async Task wrong_expected_version(int[] expectedVersions)
193194
page.Messages.Length.ShouldBe(expectedVersions.Length - 1);
194195
}
195196

197+
private static IEnumerable<string> MalformedRequests()
198+
{
199+
var messageId = Guid.NewGuid();
200+
201+
const string type = "type";
202+
203+
var jsonData = JObject.FromObject(new
204+
{
205+
property = "value"
206+
});
207+
208+
var jsonMetadata = JObject.FromObject(new
209+
{
210+
property = "metaValue"
211+
});
212+
213+
yield return string.Empty;
214+
yield return "{}";
215+
216+
yield return JObject.FromObject(new
217+
{
218+
messageId = Guid.Empty,
219+
type,
220+
jsonData,
221+
jsonMetadata
222+
}).ToString();
223+
224+
yield return JObject.FromObject(new
225+
{
226+
type,
227+
jsonData,
228+
jsonMetadata
229+
}).ToString();
230+
231+
yield return JObject.FromObject(new
232+
{
233+
messageId,
234+
jsonData,
235+
jsonMetadata
236+
}).ToString();
237+
238+
yield return $@"{{ ""messageId"": ""{messageId}"", ""type"": ""{type}"", ""jsonData"": {{ }}";
239+
yield return $@"{{ ""messageId"": ""{messageId}"", ""type"": ""{type}"", ""jsonMetaData"": {{ }}";
240+
}
241+
242+
public static IEnumerable<object[]> MalformedRequestCases()
243+
=> MalformedRequests().Select(s => new object[] { s });
244+
245+
[Theory, MemberData(nameof(MalformedRequestCases))]
246+
public async Task malformed_request_body(string malformedRequest)
247+
{
248+
using(var response = await _fixture.HttpClient.SendAsync(
249+
new HttpRequestMessage(HttpMethod.Post, $"/streams/{StreamId}")
250+
{
251+
Content = new StringContent(malformedRequest)
252+
{
253+
Headers = { ContentType = new MediaTypeHeaderValue(Constants.Headers.ContentTypes.HalJson) }
254+
}
255+
}))
256+
{
257+
response.StatusCode.ShouldBe(HttpStatusCode.BadRequest);
258+
}
259+
}
260+
196261
public void Dispose() => _fixture.Dispose();
197262
}
198263
}

src/SqlStreamStore.HAL/AppendStreamMiddleware.cs

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ namespace SqlStreamStore.HAL
33
using Microsoft.Owin;
44
using Microsoft.Owin.Builder;
55
using Owin;
6-
using SqlStreamStore.Streams;
76
using MidFunc = System.Func<System.Func<System.Collections.Generic.IDictionary<string, object>,
87
System.Threading.Tasks.Task
98
>, System.Func<System.Collections.Generic.IDictionary<string, object>,
@@ -36,16 +35,9 @@ private static MidFunc AppendStream(StreamResource stream) => next => async env
3635

3736
var options = await AppendStreamOptions.Create(context.Request, context.Request.CallCancelled);
3837

39-
try
40-
{
41-
var response = await stream.AppendMessages(options, context.Request.CallCancelled);
38+
var response = await stream.AppendMessages(options, context.Request.CallCancelled);
4239

43-
await context.WriteHalResponse(response);
44-
}
45-
catch(WrongExpectedVersionException ex)
46-
{
47-
await context.WriteWrongExpectedVersion(ex);
48-
}
40+
await context.WriteHalResponse(response);
4941
};
5042
}
5143
}

src/SqlStreamStore.HAL/AppendStreamOptions.cs

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -47,27 +47,37 @@ private AppendStreamOptions(IOwinRequest request)
4747
private AppendStreamOptions(IOwinRequest request, JArray body)
4848
: this(request)
4949
{
50-
NewStreamMessages = body.Select(newStreamMessage => new NewStreamMessage(
51-
Guid.Parse(newStreamMessage.Value<string>("messageId")),
52-
newStreamMessage.Value<string>("type"),
53-
newStreamMessage.Value<JObject>("jsonData").ToString(),
54-
newStreamMessage.Value<JObject>("jsonMetadata")?.ToString()))
50+
NewStreamMessages = body.Select(ParseNewStreamMessage)
5551
.ToArray();
5652
}
5753

5854
private AppendStreamOptions(IOwinRequest request, JObject body)
59-
: this(request)
55+
: this(request, new JArray { body })
56+
{ }
57+
58+
private static NewStreamMessage ParseNewStreamMessage(JToken newStreamMessage, int index)
6059
{
61-
NewStreamMessages = new[]
60+
if(!Guid.TryParse(newStreamMessage.Value<string>("messageId"), out var messageId))
6261
{
63-
new NewStreamMessage(
64-
Guid.Parse(body.Value<string>("messageId")),
65-
body.Value<string>("type"),
66-
body.Value<JObject>("jsonData").ToString(),
67-
body.Value<JObject>("jsonMetadata")?.ToString())
68-
};
69-
}
62+
throw new InvalidAppendRequestException($"'{nameof(messageId)}' at index {index} was improperly formatted.");
63+
};
64+
if(messageId == Guid.Empty)
65+
{
66+
throw new InvalidAppendRequestException($"'{nameof(messageId)}' at index {index} was empty.");
67+
}
68+
var type = newStreamMessage.Value<string>("type");
7069

70+
if(type == null)
71+
{
72+
throw new InvalidAppendRequestException($"'{nameof(type)}' at index {index} was not set.");
73+
}
74+
75+
return new NewStreamMessage(
76+
messageId,
77+
type,
78+
newStreamMessage.Value<JObject>("jsonData").ToString(),
79+
newStreamMessage.Value<JObject>("jsonMetadata")?.ToString());
80+
}
7181
public string StreamId { get; }
7282
public int ExpectedVersion { get; }
7383
public NewStreamMessage[] NewStreamMessages { get; }

src/SqlStreamStore.HAL/DeleteStreamMiddleware.cs

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ namespace SqlStreamStore.HAL
33
using Microsoft.Owin;
44
using Microsoft.Owin.Builder;
55
using Owin;
6-
using SqlStreamStore.Streams;
76
using MidFunc = System.Func<System.Func<System.Collections.Generic.IDictionary<string, object>,
87
System.Threading.Tasks.Task
98
>, System.Func<System.Collections.Generic.IDictionary<string, object>,
@@ -29,24 +28,17 @@ public static MidFunc UseStreamStore(IStreamStore streamStore)
2928

3029
private static bool IsStream(IOwinContext context)
3130
=> context.IsDelete() && context.Request.Path.Value?.Length > 1;
32-
33-
31+
32+
3433
private static MidFunc DeleteStream(StreamResource stream) => next => async env =>
3534
{
3635
var context = new OwinContext(env);
3736

3837
var options = new DeleteStreamOptions(context.Request);
3938

40-
try
41-
{
42-
var response = await stream.Delete(options, context.Request.CallCancelled);
39+
var response = await stream.Delete(options, context.Request.CallCancelled);
4340

44-
await context.WriteHalResponse(response);
45-
}
46-
catch(WrongExpectedVersionException ex)
47-
{
48-
await context.WriteWrongExpectedVersion(ex);
49-
}
41+
await context.WriteHalResponse(response);
5042
};
5143
}
5244
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
namespace SqlStreamStore.HAL
2+
{
3+
using System;
4+
using System.Collections.Generic;
5+
using Halcyon.HAL;
6+
using Microsoft.Owin;
7+
using Newtonsoft.Json;
8+
using SqlStreamStore.Streams;
9+
using MidFunc = System.Func<System.Func<System.Collections.Generic.IDictionary<string, object>,
10+
System.Threading.Tasks.Task
11+
>, System.Func<System.Collections.Generic.IDictionary<string, object>,
12+
System.Threading.Tasks.Task>
13+
>;
14+
15+
internal static class ExceptionHandlingMiddleware
16+
{
17+
private static readonly Func<Exception, Response> s_defaultExceptionHandler
18+
= ex => new Response(new HALResponse(new
19+
{
20+
type = ex.GetType().Name,
21+
title = "Internal Server Error",
22+
detail = ex.Message
23+
}),
24+
500);
25+
26+
private static readonly IDictionary<Type, Func<Exception, Response>> s_exceptionHandlers
27+
= new Dictionary<Type, Func<Exception, Response>>
28+
{
29+
[typeof(WrongExpectedVersionException)] = ex => new Response(new HALResponse(new
30+
{
31+
type = ex.GetType().Name,
32+
title = "Wrong expected version.",
33+
detail = ex.Message
34+
}), 409),
35+
[typeof(JsonException)] = ex => new Response(new HALResponse(new
36+
{
37+
type = ex.GetType().Name,
38+
title = "Bad format."
39+
}), 400),
40+
[typeof(InvalidAppendRequestException)] = ex => new Response(new HALResponse(new
41+
{
42+
type = ex.GetType().Name,
43+
title = "Bad format."
44+
}), 400),
45+
[typeof(Exception)] = s_defaultExceptionHandler
46+
};
47+
48+
public static MidFunc HandleExceptions => next => async env =>
49+
{
50+
try
51+
{
52+
await next(env);
53+
}
54+
catch(Exception ex)
55+
{
56+
var context = new OwinContext(env);
57+
58+
var exceptionType = ex.GetType();
59+
60+
Func<Exception, Response> exceptionHandler = null;
61+
62+
while(exceptionType != null)
63+
{
64+
if(s_exceptionHandlers.TryGetValue(exceptionType, out exceptionHandler))
65+
{
66+
break;
67+
}
68+
69+
exceptionType = exceptionType.BaseType;
70+
}
71+
72+
var response = (exceptionHandler ?? s_defaultExceptionHandler)(ex);
73+
74+
await context.WriteHalResponse(response);
75+
}
76+
};
77+
}
78+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
namespace SqlStreamStore.HAL
2+
{
3+
using System;
4+
5+
internal class InvalidAppendRequestException : Exception
6+
{
7+
public InvalidAppendRequestException(string message)
8+
: base(message)
9+
{ }
10+
11+
public InvalidAppendRequestException(string message, Exception inner)
12+
: base(message, inner)
13+
{ }
14+
}
15+
}

src/SqlStreamStore.HAL/OwinContextExtensions.cs

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,9 @@ namespace SqlStreamStore.HAL
22
{
33
using System.IO;
44
using System.Threading.Tasks;
5-
using Halcyon.HAL;
65
using Microsoft.Owin;
76
using Newtonsoft.Json;
87
using Newtonsoft.Json.Serialization;
9-
using SqlStreamStore.Streams;
108

119
internal static class OwinContextExtensions
1210
{
@@ -38,14 +36,6 @@ public static async Task WriteHalResponse(this IOwinContext context, Response re
3836
}
3937
}
4038

41-
public static Task WriteWrongExpectedVersion(this IOwinContext context, WrongExpectedVersionException ex)
42-
=> context.WriteHalResponse(new Response(new HALResponse(new
43-
{
44-
type = "WrongExpectedVersion",
45-
title = "Wrong expected version.",
46-
detail = ex.Message
47-
}), 409));
48-
4939
public static bool IsGetOrHead(this IOwinContext context)
5040
=> context.Request.Method == "GET" || context.Request.Method == "HEAD";
5141

src/SqlStreamStore.HAL/SqlStreamStoreHalMiddleware.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ public static MidFunc UseSqlStreamStoreHal(IStreamStore streamStore)
6969
throw new ArgumentNullException(nameof(streamStore));
7070

7171
var builder = new AppBuilder()
72+
.Use(ExceptionHandlingMiddleware.HandleExceptions)
7273
.Use(AccessControl)
7374
.Use(AddReasonPhrase)
7475
.Use(Index)

0 commit comments

Comments
 (0)