Fixes #206.json-serializing-nullable-fields-issue
parent
372fd804fe
commit
0b82c3bea3
@ -0,0 +1,66 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Newtonsoft.Json;
|
||||
using Recyclarr.TrashLib.Json;
|
||||
|
||||
namespace Recyclarr.Cli.Processors.ErrorHandling;
|
||||
|
||||
public sealed class ErrorResponseParser
|
||||
{
|
||||
private readonly ILogger _log;
|
||||
private readonly Func<JsonTextReader> _streamFactory;
|
||||
private readonly JsonSerializer _serializer;
|
||||
|
||||
public ErrorResponseParser(ILogger log, string responseBody)
|
||||
{
|
||||
_log = log;
|
||||
_streamFactory = () => new JsonTextReader(new StringReader(responseBody));
|
||||
_serializer = ServiceJsonSerializerFactory.Create();
|
||||
}
|
||||
|
||||
[SuppressMessage("Design", "CA1031:Do not catch general exception types")]
|
||||
public bool DeserializeList(Func<IEnumerable<dynamic>, IEnumerable<object>> expr)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var stream = _streamFactory();
|
||||
var value = _serializer.Deserialize<List<dynamic>>(stream);
|
||||
if (value is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var parsed = expr(value);
|
||||
foreach (var s in parsed)
|
||||
{
|
||||
_log.Error("Reason: {Message:l}", (string) s);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[SuppressMessage("Design", "CA1031:Do not catch general exception types")]
|
||||
public bool Deserialize(Func<dynamic, object> expr)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var stream = _streamFactory();
|
||||
var value = _serializer.Deserialize<dynamic>(stream);
|
||||
if (value is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_log.Error("Reason: {Message:l}", (string) expr(value));
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Recyclarr.Common.Extensions;
|
||||
|
||||
namespace Recyclarr.Cli.Processors.ErrorHandling;
|
||||
|
||||
public class FlurlHttpExceptionHandler : IFlurlHttpExceptionHandler
|
||||
{
|
||||
private readonly ILogger _log;
|
||||
|
||||
public FlurlHttpExceptionHandler(ILogger log)
|
||||
{
|
||||
_log = log;
|
||||
}
|
||||
|
||||
[SuppressMessage("Design", "CA1031:Do not catch general exception types")]
|
||||
public async Task ProcessServiceErrorMessages(IServiceErrorMessageExtractor extractor)
|
||||
{
|
||||
var responseBody = await extractor.GetErrorMessage();
|
||||
var parser = new ErrorResponseParser(_log, responseBody);
|
||||
|
||||
if (parser.DeserializeList(s => s
|
||||
.Select(x => (string) x.errorMessage)
|
||||
.NotNull(x => !string.IsNullOrEmpty(x))))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (parser.Deserialize(s => s.message))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Last resort
|
||||
_log.Error("Reason: Unable to determine. Please report this as a bug and attach your `verbose.log` file.");
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
namespace Recyclarr.Cli.Processors.ErrorHandling;
|
||||
|
||||
public interface IFlurlHttpExceptionHandler
|
||||
{
|
||||
Task ProcessServiceErrorMessages(IServiceErrorMessageExtractor extractor);
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
namespace Recyclarr.Cli.Processors.ErrorHandling;
|
||||
|
||||
public interface IServiceErrorMessageExtractor
|
||||
{
|
||||
Task<string> GetErrorMessage();
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
using Flurl.Http;
|
||||
|
||||
namespace Recyclarr.Cli.Processors.ErrorHandling;
|
||||
|
||||
public class ServiceErrorMessageExtractor : IServiceErrorMessageExtractor
|
||||
{
|
||||
private readonly FlurlHttpException _e;
|
||||
|
||||
public ServiceErrorMessageExtractor(FlurlHttpException e)
|
||||
{
|
||||
_e = e;
|
||||
}
|
||||
|
||||
public async Task<string> GetErrorMessage()
|
||||
{
|
||||
return await _e.GetResponseStringAsync();
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
{
|
||||
"message": "database is locked\ndatabase is locked",
|
||||
"description": "code = Busy (5), message = System.Data.SQLite.SQLiteException (0x87AF00AA): database is locked\ndatabase is locked\n at System.Data.SQLite.SQLite3.Prepare(SQLiteConnection cnn, SQLiteCommand command, String strSql, SQLiteStatement previous, UInt32 timeoutMS, String& strRemain)\n at System.Data.SQLite.SQLiteCommand.BuildNextCommand()\n at System.Data.SQLite.SQLiteDataReader.NextResult()\n at System.Data.SQLite.SQLiteDataReader..ctor(SQLiteCommand cmd, CommandBehavior behave)\n at System.Data.SQLite.SQLiteCommand.ExecuteReader(CommandBehavior behavior)\n at System.Data.SQLite.SQLiteCommand.ExecuteNonQuery(CommandBehavior behavior)\n at System.Data.SQLite.SQLiteConnection.Open()\n at NzbDrone.Core.Datastore.DbFactory.<>c__DisplayClass9_0.<Create>b__0() in ./Radarr.Core/Datastore/DbFactory.cs:line 104\n at NzbDrone.Core.Datastore.BasicRepository`1.UpdateMany(IList`1 models) in ./Radarr.Core/Datastore/BasicRepository.cs:line 246\n at NzbDrone.Core.Qualities.QualityDefinitionService.UpdateMany(List`1 qualityDefinitions) in ./Radarr.Core/Qualities/QualityDefinitionService.cs:line 49\n at Radarr.Api.V3.Qualities.QualityDefinitionController.UpdateMany(List`1 resource) in ./Radarr.Api.V3/Qualities/QualityDefinitionController.cs:line 52\n at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.SyncObjectResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)\n at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeActionMethodAsync()\n at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)\n at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeNextActionFilterAsync()\n--- End of stack trace from previous location ---\n at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)\n at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)\n at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()\n--- End of stack trace from previous location ---\n at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)\n at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)\n at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)\n at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)\n at Radarr.Http.Middleware.BufferingMiddleware.InvokeAsync(HttpContext context) in ./Radarr.Http/Middleware/BufferingMiddleware.cs:line 28\n at Radarr.Http.Middleware.IfModifiedMiddleware.InvokeAsync(HttpContext context) in ./Radarr.Http/Middleware/IfModifiedMiddleware.cs:line 41\n at Radarr.Http.Middleware.CacheHeaderMiddleware.InvokeAsync(HttpContext context) in ./Radarr.Http/Middleware/CacheHeaderMiddleware.cs:line 33\n at Radarr.Http.Middleware.StartingUpMiddleware.InvokeAsync(HttpContext context) in ./Radarr.Http/Middleware/StartingUpMiddleware.cs:line 38\n at Radarr.Http.Middleware.UrlBaseMiddleware.InvokeAsync(HttpContext context) in ./Radarr.Http/Middleware/UrlBaseMiddleware.cs:line 27\n at Radarr.Http.Middleware.VersionMiddleware.InvokeAsync(HttpContext context) in ./Radarr.Http/Middleware/VersionMiddleware.cs:line 29\n at Microsoft.AspNetCore.ResponseCompression.ResponseCompressionMiddleware.InvokeCore(HttpContext context)\n at Microsoft.AspNetCore.Authorization.Policy.AuthorizationMiddlewareResultHandler.HandleAsync(RequestDelegate next, HttpContext context, AuthorizationPolicy policy, PolicyAuthorizationResult authorizeResult)\n at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)\n at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)\n at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.<Invoke>g__Awaited|6_0(ExceptionHandlerMiddleware middleware, HttpContext context, Task task)"
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
[
|
||||
{
|
||||
"errorMessage": "error one"
|
||||
},
|
||||
{
|
||||
"errorMessage": "error two"
|
||||
}
|
||||
]
|
@ -0,0 +1,57 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Recyclarr.Cli.Processors.ErrorHandling;
|
||||
using Recyclarr.Common;
|
||||
using Recyclarr.TestLibrary;
|
||||
|
||||
namespace Recyclarr.Cli.Tests.Processors.ErrorHandling;
|
||||
|
||||
[TestFixture]
|
||||
[Parallelizable(ParallelScope.All)]
|
||||
[SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope")]
|
||||
public class FlurlHttpExceptionHandlerTest
|
||||
{
|
||||
[Test, AutoMockData]
|
||||
public async Task Http_exception_print_validation_errors(
|
||||
[Frozen(Matching.ImplementedInterfaces)] TestableLogger log,
|
||||
IServiceErrorMessageExtractor extractor,
|
||||
FlurlHttpExceptionHandler sut)
|
||||
{
|
||||
var resourceReader = new ResourceDataReader(typeof(FlurlHttpExceptionHandlerTest), "Data");
|
||||
var responseContent = resourceReader.ReadData("validation_error.json");
|
||||
|
||||
extractor.GetErrorMessage().Returns(responseContent);
|
||||
await sut.ProcessServiceErrorMessages(extractor);
|
||||
|
||||
var logs = log.Messages;
|
||||
|
||||
logs.Should().BeEquivalentTo(new[]
|
||||
{
|
||||
"Reason: error one",
|
||||
"Reason: error two"
|
||||
},
|
||||
o => o.WithStrictOrdering()
|
||||
);
|
||||
}
|
||||
|
||||
[Test, AutoMockData]
|
||||
public async Task Http_exception_print_plain_message(
|
||||
[Frozen(Matching.ImplementedInterfaces)] TestableLogger log,
|
||||
IServiceErrorMessageExtractor extractor,
|
||||
FlurlHttpExceptionHandler sut)
|
||||
{
|
||||
var resourceReader = new ResourceDataReader(typeof(FlurlHttpExceptionHandlerTest), "Data");
|
||||
var responseContent = resourceReader.ReadData("database_locked_error.json");
|
||||
|
||||
extractor.GetErrorMessage().Returns(responseContent);
|
||||
await sut.ProcessServiceErrorMessages(extractor);
|
||||
|
||||
var logs = log.Messages;
|
||||
|
||||
logs.Should().BeEquivalentTo(new[]
|
||||
{
|
||||
"Reason: database is locked\ndatabase is locked"
|
||||
},
|
||||
o => o.WithStrictOrdering()
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in new issue