Skip to content

Commit 2d74c3c

Browse files
committed
Fixed:
1. Report 400 instead of 500 if webhook fails 2. Validate entire object graph when creating settings 3. Correct processing exceptions in webhook handler
1 parent f0141e1 commit 2d74c3c

14 files changed

+224
-107
lines changed

src/Zeus/Controllers/ErrorController.cs

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ static HttpStatusCode GetStatusCode(Exception exception)
2626
ConflictException _ => HttpStatusCode.Conflict,
2727
NotFoundException _ => HttpStatusCode.NotFound,
2828
UnauthorizedAccessException _ => HttpStatusCode.Unauthorized,
29+
AlertManagerUpdateException _ => HttpStatusCode.BadRequest,
2930
_ => HttpStatusCode.InternalServerError
3031
};
3132
}

src/Zeus/Features/Alerting/AlertingFeature.cs

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
using Microsoft.AspNetCore.Builder;
1+
using MediatR;
2+
using Microsoft.AspNetCore.Builder;
23
using Microsoft.Extensions.Configuration;
34
using Microsoft.Extensions.DependencyInjection;
45
using Microsoft.Extensions.DependencyInjection.Extensions;
56
using Microsoft.Extensions.Hosting;
67
using Microsoft.Extensions.Options;
8+
using Zeus.Handlers.Webhook;
79
using Zeus.Services.Templating;
810
using Zeus.Services.Templating.Scriban;
911
using Zeus.Shared.AppFeature;
@@ -49,6 +51,8 @@ public override void Configure(IServiceCollection services, IAppFeatureCollectio
4951
if (options.Channels.Store.Predefined != null)
5052
services.AddSingleton<IChannelStore>(sp => new InMemoryChannelStore(
5153
options.Channels.Store.Predefined));
54+
55+
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(WrapAlertManagerUpdateExceptionsBehavior<,>));
5256
}
5357

5458
/// <inheritdoc />

src/Zeus/Handlers/Webhook/AlertManagerUpdateRequestHandler.cs

+4-1
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,10 @@ protected override async Task Handle(AlertManagerUpdateRequest request, Cancella
8383
var results = await Try.WhenAll(sendTasks);
8484
if (results.HasFaults())
8585
{
86-
var exception = new AggregateException(results.Select(s => s.Exception));
86+
var exceptions = results.Where(s => s.IsFaulted)
87+
.Select(s => s.Exception);
88+
89+
var exception = new AggregateException(exceptions);
8790
throw exception;
8891
}
8992
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using System;
2+
using Zeus.Shared.Exceptions;
3+
using Zeus.Shared.Mediatr;
4+
5+
namespace Zeus.Handlers.Webhook
6+
{
7+
public class WrapAlertManagerUpdateExceptionsBehavior<TRequest, TResponse>
8+
: WrapExceptionsBehavior<TRequest, TResponse>
9+
{
10+
/// <inheritdoc />
11+
protected override bool CanWrap(TRequest request)
12+
{
13+
return typeof(TRequest) == typeof(AlertManagerUpdateRequest);
14+
}
15+
16+
/// <inheritdoc />
17+
protected override Exception Wrap(TRequest request, Exception source)
18+
{
19+
return new AlertManagerUpdateException("Exception occured while handling alertmanager webhook request", source);
20+
}
21+
}
22+
}

src/Zeus/Program.cs

-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
using Microsoft.AspNetCore.Hosting;
33
using Microsoft.Extensions.Hosting;
44
using Serilog;
5-
using Serilog.Events;
65
using Serilog.Sinks.SystemConsole.Themes;
76
using Zeus.Shared.Serilog.Enrichers;
87

src/Zeus/Shared/AppFeature/Internal/AppFeatureOptions.cs

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using Microsoft.Extensions.Options;
3+
using Zeus.Shared.Validation;
34

45
namespace Zeus.Shared.AppFeature.Internal
56
{
@@ -17,6 +18,9 @@ public AppFeatureOptions(Action<TOptions> configureOptions)
1718
{
1819
var options = new TOptions();
1920
configureOptions(options);
21+
22+
DataAnnotationsValidator.EnsureValid(options);
23+
2024
return options;
2125
});
2226
}

src/Zeus/Shared/Exceptions/AlertHandlingException.cs

-27
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using System;
2+
using System.Runtime.Serialization;
3+
4+
namespace Zeus.Shared.Exceptions
5+
{
6+
[Serializable]
7+
public class AlertManagerUpdateException : Exception
8+
{
9+
public AlertManagerUpdateException()
10+
{
11+
}
12+
13+
protected AlertManagerUpdateException(SerializationInfo info, StreamingContext context)
14+
: base(info, context)
15+
{
16+
}
17+
18+
public AlertManagerUpdateException(string message) : base(message)
19+
{
20+
}
21+
22+
public AlertManagerUpdateException(string message, Exception innerException)
23+
: base(message, innerException)
24+
{
25+
}
26+
}
27+
}
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
11
using System;
2-
using System.Collections.Generic;
3-
using System.ComponentModel.DataAnnotations;
4-
using System.Linq;
52
using Microsoft.Extensions.Configuration;
63
using Zeus.Shared.Exceptions;
74

@@ -27,33 +24,6 @@ public static IConfigurationSection GetRequiredSection(this IConfiguration confi
2724
return section;
2825
}
2926

30-
public static TOptions CreateOptions<TOptions>(this IConfigurationSection section)
31-
where TOptions : class, new()
32-
{
33-
var options = new TOptions();
34-
section.Bind(options);
35-
36-
ValidateObject(options);
37-
38-
return options;
39-
}
40-
41-
public static TOptions CreateOptions<TOptions>(this IConfiguration configuration, string name)
42-
where TOptions : class, new()
43-
{
44-
if (name == null)
45-
throw new ArgumentNullException(nameof(name));
46-
47-
var section = configuration.GetRequiredSection(name);
48-
var options = new TOptions();
49-
50-
section.Bind(options);
51-
52-
ValidateObject(options);
53-
54-
return options;
55-
}
56-
5727
public static Action<TOptions> CreateBinder<TOptions>(this IConfiguration configuration, string name,
5828
bool required)
5929
{
@@ -69,23 +39,5 @@ public static Action<TOptions> CreateBinder<TOptions>(this IConfiguration config
6939
}
7040
};
7141
}
72-
73-
private static void ValidateObject(object instance)
74-
{
75-
if (instance == null)
76-
throw new ArgumentNullException(nameof(instance));
77-
78-
var context = new ValidationContext(instance);
79-
var validationResults = new List<ValidationResult>();
80-
81-
if (Validator.TryValidateObject(instance, context, validationResults, validateAllProperties: true))
82-
return;
83-
84-
var errors = validationResults.Where(s => !string.IsNullOrEmpty(s.ErrorMessage))
85-
.Select(s => s.ErrorMessage);
86-
87-
var errorMessage = string.Join(';', errors);
88-
throw new ConfigurationException($"Errors occured while validating object '{instance.GetType().Name}': {errorMessage}");
89-
}
9042
}
9143
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using System;
2+
using System.Threading;
3+
using System.Threading.Tasks;
4+
using MediatR;
5+
6+
namespace Zeus.Shared.Mediatr
7+
{
8+
public abstract class WrapExceptionsBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
9+
{
10+
/// <inheritdoc />
11+
public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
12+
{
13+
if (!CanWrap(request))
14+
return await next();
15+
16+
try
17+
{
18+
return await next();
19+
}
20+
catch (Exception e)
21+
{
22+
var resultException = Wrap(request, e);
23+
if (resultException != null)
24+
throw resultException;
25+
26+
// Throw if cant wrap
27+
throw;
28+
}
29+
}
30+
31+
protected abstract bool CanWrap(TRequest request);
32+
33+
protected abstract Exception Wrap(TRequest request, Exception source);
34+
}
35+
}

src/Zeus/Shared/Try/Try.cs

+7-12
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,13 @@ public static class Try
99
{
1010
public static Task<TryResult<T>[]> WhenAll<T>(IEnumerable<Task<T>> tasks)
1111
{
12-
return Task.WhenAll(tasks.Select(async t =>
13-
{
14-
try
15-
{
16-
var result = await t;
17-
return new TryResult<T>(result);
18-
}
19-
catch (Exception e)
20-
{
21-
return new TryResult<T>(e);
22-
}
23-
}));
12+
var wrappedTasks = tasks.Select(
13+
task => task.ContinueWith(
14+
t => t.IsFaulted
15+
? new TryResult<T>(t.Exception)
16+
: new TryResult<T>(t.Result)));
17+
18+
return Task.WhenAll(wrappedTasks);
2419
}
2520

2621
public static async Task<TryResult> ExecuteAsync(Func<Task> asyncAction)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.ComponentModel.DataAnnotations;
4+
using System.Linq;
5+
using System.Runtime.CompilerServices;
6+
using Zeus.Shared.Exceptions;
7+
8+
namespace Zeus.Shared.Validation
9+
{
10+
public static class DataAnnotationsValidator
11+
{
12+
public static void EnsureValid(object instance)
13+
{
14+
if (instance == null)
15+
throw new ArgumentNullException(nameof(instance));
16+
17+
var results = new List<ValidationResult>();
18+
if (TryValidateRecursive(instance, ref results))
19+
return;
20+
21+
var errors = results.Where(s => !string.IsNullOrEmpty(s.ErrorMessage))
22+
.Select(s => s.ErrorMessage);
23+
24+
var errorMessage = string.Join(';', errors);
25+
throw new ConfigurationException($"Errors occured while validating object '{instance.GetType().Name}': {errorMessage}");
26+
}
27+
28+
private static bool TryValidateRecursive(object instance, ref List<ValidationResult> results)
29+
{
30+
if (instance == null)
31+
throw new ArgumentNullException(nameof(instance));
32+
33+
if (results == null)
34+
results = new List<ValidationResult>();
35+
36+
var validationContext = new ValidationContext(instance);
37+
var isValid = Validator.TryValidateObject(instance, validationContext, results, validateAllProperties: true);
38+
39+
var properties = instance.GetType()
40+
.GetProperties()
41+
.Where(prop => prop.CanRead && prop.GetIndexParameters().Length == 0)
42+
.Where(prop => CanValidate(prop.PropertyType))
43+
.ToArray();
44+
45+
foreach (var property in properties)
46+
{
47+
var value = property.GetValue(instance);
48+
if (value == null)
49+
continue;
50+
51+
var enumerable = value as IEnumerable<object> ?? new[] { value };
52+
53+
foreach (var toValidate in enumerable)
54+
{
55+
var nestedResults = new List<ValidationResult>();
56+
if (TryValidateRecursive(toValidate, ref nestedResults))
57+
{
58+
continue;
59+
}
60+
61+
isValid = false;
62+
63+
results.AddRange(nestedResults
64+
.Select(result => new ValidationResult(
65+
result.ErrorMessage, result.MemberNames
66+
.Select(x => property.Name + '.' + x))));
67+
}
68+
}
69+
70+
71+
return isValid;
72+
}
73+
74+
/// <summary>
75+
/// Returns whether the given <paramref name="type"/> can be validated
76+
/// </summary>
77+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
78+
private static bool CanValidate(Type type)
79+
{
80+
while (true)
81+
{
82+
if (type == null)
83+
return false;
84+
85+
if (type == typeof(string))
86+
return false;
87+
88+
if (type.IsValueType)
89+
return false;
90+
91+
if (!type.IsArray || !type.HasElementType)
92+
return true;
93+
94+
var elementType = type.GetElementType();
95+
type = elementType;
96+
}
97+
}
98+
}
99+
}

0 commit comments

Comments
 (0)