diff --git a/backend/src/Squidex.Web/ApiExceptionFilterAttribute.cs b/backend/src/Squidex.Web/ApiExceptionFilterAttribute.cs index 41df439aa..11eacda19 100644 --- a/backend/src/Squidex.Web/ApiExceptionFilterAttribute.cs +++ b/backend/src/Squidex.Web/ApiExceptionFilterAttribute.cs @@ -9,7 +9,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.DependencyInjection; -using Squidex.Infrastructure.Log; +using Squidex.Web.Pipeline; namespace Squidex.Web { @@ -33,12 +33,9 @@ namespace Squidex.Web if (!wellKnown) { - var log = context.HttpContext.RequestServices.GetService(); + var exceptionHandler = context.HttpContext.RequestServices.GetService(); - if (log != null) - { - log.LogError(context.Exception, w => w.WriteProperty("status", "UnhandledException")); - } + exceptionHandler?.Handle(context.Exception); } context.Result = GetResult(error); diff --git a/backend/src/Squidex.Web/Pipeline/DefaultExceptionHandler.cs b/backend/src/Squidex.Web/Pipeline/DefaultExceptionHandler.cs new file mode 100644 index 000000000..7ae634fee --- /dev/null +++ b/backend/src/Squidex.Web/Pipeline/DefaultExceptionHandler.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; + +namespace Squidex.Web.Pipeline +{ + public class DefaultExceptionHandler : IExceptionHandler + { + private readonly ISemanticLog log; + + public DefaultExceptionHandler(ISemanticLog log) + { + Guard.NotNull(log); + + this.log = log; + } + + public void Handle(Exception ex) + { + log.LogError(ex, w => w.WriteProperty("status", "UnhandledException")); + } + } +} diff --git a/backend/src/Squidex.Web/Pipeline/IExceptionHandler.cs b/backend/src/Squidex.Web/Pipeline/IExceptionHandler.cs new file mode 100644 index 000000000..704768ddc --- /dev/null +++ b/backend/src/Squidex.Web/Pipeline/IExceptionHandler.cs @@ -0,0 +1,16 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; + +namespace Squidex.Web.Pipeline +{ + public interface IExceptionHandler + { + void Handle(Exception exception); + } +} diff --git a/backend/src/Squidex.Web/Pipeline/RequestExceptionMiddleware.cs b/backend/src/Squidex.Web/Pipeline/RequestExceptionMiddleware.cs new file mode 100644 index 000000000..06dac9cec --- /dev/null +++ b/backend/src/Squidex.Web/Pipeline/RequestExceptionMiddleware.cs @@ -0,0 +1,42 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Security; + +namespace Squidex.Web.Pipeline +{ + public sealed class RequestExceptionMiddleware : IMiddleware + { + private readonly IExceptionHandler exceptionHandler; + + public RequestExceptionMiddleware(IExceptionHandler exceptionHandler) + { + Guard.NotNull(exceptionHandler); + + this.exceptionHandler = exceptionHandler; + } + + public async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + try + { + await next(context); + } + catch (Exception ex) + { + exceptionHandler.Handle(ex); + + context.Response.StatusCode = 500; + } + } + } +} diff --git a/backend/src/Squidex.Web/Pipeline/RequestLogPerformanceMiddleware.cs b/backend/src/Squidex.Web/Pipeline/RequestLogPerformanceMiddleware.cs index e18e678eb..27e3d39f3 100644 --- a/backend/src/Squidex.Web/Pipeline/RequestLogPerformanceMiddleware.cs +++ b/backend/src/Squidex.Web/Pipeline/RequestLogPerformanceMiddleware.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; @@ -40,12 +39,6 @@ namespace Squidex.Web.Pipeline { await next(context); } - catch (Exception ex) - { - log.LogError(ex, w => w.WriteProperty("status", "UnhandledException")); - - context.Response.StatusCode = 500; - } finally { var elapsedMs = watch.Stop(); diff --git a/backend/src/Squidex/Config/Web/WebServices.cs b/backend/src/Squidex/Config/Web/WebServices.cs index b2b8717c1..419691402 100644 --- a/backend/src/Squidex/Config/Web/WebServices.cs +++ b/backend/src/Squidex/Config/Web/WebServices.cs @@ -61,6 +61,9 @@ namespace Squidex.Config.Web services.AddSingletonAs() .As(); + services.AddSingletonAs() + .AsOptional(); + services.Configure(options => { options.SuppressModelStateInvalidFilter = true; diff --git a/backend/tests/Squidex.Web.Tests/ApiExceptionFilterAttributeTests.cs b/backend/tests/Squidex.Web.Tests/ApiExceptionFilterAttributeTests.cs index e2eea0a51..89220f151 100644 --- a/backend/tests/Squidex.Web.Tests/ApiExceptionFilterAttributeTests.cs +++ b/backend/tests/Squidex.Web.Tests/ApiExceptionFilterAttributeTests.cs @@ -19,13 +19,14 @@ using Microsoft.AspNetCore.Routing; using Squidex.Infrastructure; using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Validation; +using Squidex.Web.Pipeline; using Xunit; namespace Squidex.Web { public class ApiExceptionFilterAttributeTests { - private readonly ISemanticLog log = A.Fake(); + private readonly IExceptionHandler exceptionHandler = A.Fake(); private readonly ApiExceptionFilterAttribute sut = new ApiExceptionFilterAttribute(); [Fact] @@ -49,7 +50,7 @@ namespace Squidex.Web Assert.Equal(new[] { "Error1", "P: Error2", "P1, P2: Error3" }, ((ErrorDto)result.Value).Details); - A.CallTo(() => log.Log(SemanticLogLevel.Error, None.Value, A>._)) + A.CallTo(() => exceptionHandler.Handle(A._)) .MustNotHaveHappened(); } @@ -62,7 +63,7 @@ namespace Squidex.Web Assert.IsType(context.Result); - A.CallTo(() => log.Log(SemanticLogLevel.Error, None.Value, A>._)) + A.CallTo(() => exceptionHandler.Handle(A._)) .MustNotHaveHappened(); } @@ -75,7 +76,7 @@ namespace Squidex.Web Validate(500, context.Result, null); - A.CallTo(() => log.Log(SemanticLogLevel.Error, None.Value, A>._)) + A.CallTo(() => exceptionHandler.Handle(A._)) .MustHaveHappened(); } @@ -88,7 +89,7 @@ namespace Squidex.Web Validate(400, context.Result, context.Exception); - A.CallTo(() => log.Log(SemanticLogLevel.Error, None.Value, A>._)) + A.CallTo(() => exceptionHandler.Handle(A._)) .MustNotHaveHappened(); } @@ -101,7 +102,7 @@ namespace Squidex.Web Validate(400, context.Result, context.Exception); - A.CallTo(() => log.Log(SemanticLogLevel.Error, None.Value, A>._)) + A.CallTo(() => exceptionHandler.Handle(A._)) .MustNotHaveHappened(); } @@ -114,7 +115,7 @@ namespace Squidex.Web Validate(412, context.Result, context.Exception); - A.CallTo(() => log.Log(SemanticLogLevel.Error, None.Value, A>._)) + A.CallTo(() => exceptionHandler.Handle(A._)) .MustNotHaveHappened(); } @@ -127,7 +128,7 @@ namespace Squidex.Web Validate(403, context.Result, context.Exception); - A.CallTo(() => log.Log(SemanticLogLevel.Error, None.Value, A>._)) + A.CallTo(() => exceptionHandler.Handle(A._)) .MustNotHaveHappened(); } @@ -140,7 +141,7 @@ namespace Squidex.Web Validate(403, context.Result, null); - A.CallTo(() => log.Log(SemanticLogLevel.Error, None.Value, A>._)) + A.CallTo(() => exceptionHandler.Handle(A._)) .MustHaveHappened(); } @@ -153,7 +154,7 @@ namespace Squidex.Web Validate(403, context.Result, null); - A.CallTo(() => log.Log(SemanticLogLevel.Error, None.Value, A>._)) + A.CallTo(() => exceptionHandler.Handle(A._)) .MustNotHaveHappened(); } @@ -190,7 +191,7 @@ namespace Squidex.Web var services = A.Fake(); A.CallTo(() => services.GetService(typeof(ISemanticLog))) - .Returns(log); + .Returns(exceptionHandler); var httpContext = new DefaultHttpContext {