From 4f5946bc67583e56d2efdc26a1b6551d454eef9e Mon Sep 17 00:00:00 2001 From: Robert Dailey Date: Wed, 20 Sep 2023 16:23:22 -0500 Subject: [PATCH] fix: Parse error messages from service with title & errors list --- .../ErrorHandling/ErrorResponseParser.cs | 31 +++++++++++++++++++ .../FlurlHttpExceptionHandler.cs | 8 +++++ .../ErrorHandling/Data/title_errors_list.json | 11 +++++++ .../FlurlHttpExceptionHandlerTest.cs | 24 ++++++++++++++ 4 files changed, 74 insertions(+) create mode 100644 src/tests/Recyclarr.Cli.Tests/Processors/ErrorHandling/Data/title_errors_list.json diff --git a/src/Recyclarr.Cli/Processors/ErrorHandling/ErrorResponseParser.cs b/src/Recyclarr.Cli/Processors/ErrorHandling/ErrorResponseParser.cs index a16dbd00..ec6d9ac6 100644 --- a/src/Recyclarr.Cli/Processors/ErrorHandling/ErrorResponseParser.cs +++ b/src/Recyclarr.Cli/Processors/ErrorHandling/ErrorResponseParser.cs @@ -1,6 +1,7 @@ using System.Diagnostics.CodeAnalysis; using System.Text; using System.Text.Json; +using JetBrains.Annotations; using Recyclarr.Json; namespace Recyclarr.Cli.Processors.ErrorHandling; @@ -64,4 +65,34 @@ public sealed class ErrorResponseParser return false; } } + + [UsedImplicitly(ImplicitUseKindFlags.Assign)] + private sealed record ServiceErrorsList(string Title, Dictionary> Errors); + + [SuppressMessage("Design", "CA1031:Do not catch general exception types")] + public bool DeserializeServiceErrorList() + { + try + { + using var stream = _streamFactory(); + var value = JsonSerializer.Deserialize(stream, _jsonSettings); + if (value is null) + { + return false; + } + + _log.Error("Error message from remote service: {Message:l}", value.Title); + + foreach (var (topic, msg) in value.Errors.SelectMany(x => x.Value.Select(y => (x.Key, Msg: y)))) + { + _log.Error("{Topic:l}: {Message:l}", topic, msg); + } + + return true; + } + catch + { + return false; + } + } } diff --git a/src/Recyclarr.Cli/Processors/ErrorHandling/FlurlHttpExceptionHandler.cs b/src/Recyclarr.Cli/Processors/ErrorHandling/FlurlHttpExceptionHandler.cs index 2c79d0bb..3b4fb533 100644 --- a/src/Recyclarr.Cli/Processors/ErrorHandling/FlurlHttpExceptionHandler.cs +++ b/src/Recyclarr.Cli/Processors/ErrorHandling/FlurlHttpExceptionHandler.cs @@ -33,6 +33,7 @@ public class FlurlHttpExceptionHandler : IFlurlHttpExceptionHandler { var parser = new ErrorResponseParser(_log, responseBody); + // Try to parse validation errors if (parser.DeserializeList(s => s .Select(x => x.GetProperty("errorMessage").GetString()) .NotNull(x => !string.IsNullOrEmpty(x)))) @@ -40,11 +41,18 @@ public class FlurlHttpExceptionHandler : IFlurlHttpExceptionHandler return; } + // Try to parse single error message if (parser.Deserialize(s => s.GetProperty("message").GetString())) { return; } + // A list of errors with a title + if (parser.DeserializeServiceErrorList()) + { + return; + } + // Last resort _log.Error("Reason: Unable to determine. Please report this as a bug and attach your `verbose.log` file."); } diff --git a/src/tests/Recyclarr.Cli.Tests/Processors/ErrorHandling/Data/title_errors_list.json b/src/tests/Recyclarr.Cli.Tests/Processors/ErrorHandling/Data/title_errors_list.json new file mode 100644 index 00000000..af3bc38d --- /dev/null +++ b/src/tests/Recyclarr.Cli.Tests/Processors/ErrorHandling/Data/title_errors_list.json @@ -0,0 +1,11 @@ +{ + "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1", + "title": "One or more validation errors occurred.", + "status": 400, + "traceId": "00-e2ebf7693f107cf617432d815b429382-d07ea6c6fd009421-00", + "errors": { + "$.items[0].id": [ + "The JSON value could not be converted to System.Int32. Path: $.items[0].id | LineNumber: 0 | BytePositionInLine: 3789." + ] + } +} diff --git a/src/tests/Recyclarr.Cli.Tests/Processors/ErrorHandling/FlurlHttpExceptionHandlerTest.cs b/src/tests/Recyclarr.Cli.Tests/Processors/ErrorHandling/FlurlHttpExceptionHandlerTest.cs index a18b6e3a..f45c02c5 100644 --- a/src/tests/Recyclarr.Cli.Tests/Processors/ErrorHandling/FlurlHttpExceptionHandlerTest.cs +++ b/src/tests/Recyclarr.Cli.Tests/Processors/ErrorHandling/FlurlHttpExceptionHandlerTest.cs @@ -56,4 +56,28 @@ public class FlurlHttpExceptionHandlerTest logs.Should().HaveCount(expectedSubstrings.Length); logs.Zip(expectedSubstrings).Should().OnlyContain(pair => pair.First.Contains(pair.Second)); } + + [Test, AutoMockData] + public async Task Http_exception_print_title_and_errors_list( + [Frozen(Matching.ImplementedInterfaces)] TestableLogger log, + IServiceErrorMessageExtractor extractor, + FlurlHttpExceptionHandler sut) + { + var resourceReader = new ResourceDataReader(typeof(FlurlHttpExceptionHandlerTest), "Data"); + var responseContent = resourceReader.ReadData("title_errors_list.json"); + + extractor.GetErrorMessage().Returns(responseContent); + await sut.ProcessServiceErrorMessages(extractor); + + var logs = log.Messages.ToList(); + + var expectedSubstrings = new[] + { + "One or more validation errors occurred", + "$.items[0].id: The JSON value could not be converted to System.Int32. Path: $.items[0].id | LineNumber: 0 | BytePositionInLine: 3789." + }; + + logs.Should().HaveCount(expectedSubstrings.Length); + logs.Zip(expectedSubstrings).Should().OnlyContain(pair => pair.First.Contains(pair.Second)); + } }