diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/AbpIoSourceCodeStore.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/AbpIoSourceCodeStore.cs index a594613b44..257e12d0dc 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/AbpIoSourceCodeStore.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/AbpIoSourceCodeStore.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net; using System.Net.Http; using System.Text; using System.Text.RegularExpressions; @@ -240,14 +241,14 @@ public class AbpIoSourceCodeStore : ISourceCodeStore, ITransientDependency using (var response = await client.PostAsync(url, stringContent, _cliHttpClientFactory.GetCancellationToken(TimeSpan.FromMinutes(10)))) { - await RemoteServiceExceptionHandler.EnsureSuccessfulHttpResponseAsync(response); + await EnsureAbpIoSuccessfulResponseAsync(response); var result = await response.Content.ReadAsStringAsync(); return JsonSerializer.Deserialize(result).Version; } } - catch (Exception ex) + catch (Exception ex) when (ex is not CliUsageException) { - Console.WriteLine("Error occured while getting the latest version from {0} : {1}", url, ex.Message); + Console.WriteLine("Error occurred while getting the latest version from {0} : {1}", url, ex.Message); return null; } } @@ -273,17 +274,39 @@ public class AbpIoSourceCodeStore : ISourceCodeStore, ITransientDependency using (var response = await client.PostAsync(url, stringContent, _cliHttpClientFactory.GetCancellationToken(TimeSpan.FromMinutes(10)))) { - await RemoteServiceExceptionHandler.EnsureSuccessfulHttpResponseAsync(response); + await EnsureAbpIoSuccessfulResponseAsync(response); var result = await response.Content.ReadAsStringAsync(); return JsonSerializer.Deserialize(result).Version; } } - catch (Exception) + catch (Exception ex) when (ex is not CliUsageException) { return null; } } + private async Task EnsureAbpIoSuccessfulResponseAsync(HttpResponseMessage responseMessage) + { + if (responseMessage is { StatusCode: HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden }) + { + var message = $"Remote server returns '{(int)responseMessage.StatusCode}-{responseMessage.ReasonPhrase}'. "; + + var serverError = await RemoteServiceExceptionHandler.GetAbpRemoteServiceErrorAsync(responseMessage); + if (!string.IsNullOrWhiteSpace(serverError)) + { + message += serverError + " "; + } + + message += $"Authentication or license check failed while accessing {CliUrls.WwwAbpIo}. " + + "Please make sure you are logged in with `abp login ` and your ABP commercial license is active and covers the requested version. " + + $"You can check your license at {CliUrls.WwwAbpIo}my-organizations"; + + throw new CliUsageException(message); + } + + await RemoteServiceExceptionHandler.EnsureSuccessfulHttpResponseAsync(responseMessage); + } + private async Task IsVersionExists(string templateName, string version) { var url = $"{CliUrls.WwwAbpIo}api/download/all-versions?includePreReleases=true"; @@ -313,6 +336,8 @@ public class AbpIoSourceCodeStore : ISourceCodeStore, ITransientDependency private async Task DownloadSourceCodeContentAsync(SourceCodeDownloadInputDto input) { var url = $"{CliUrls.WwwAbpIo}api/download/{input.Type}/"; + var isAbpIoDownload = input.TemplateSource.IsNullOrWhiteSpace(); + var downloadUrl = isAbpIoDownload ? url : input.TemplateSource; HttpResponseMessage responseMessage = null; @@ -320,7 +345,7 @@ public class AbpIoSourceCodeStore : ISourceCodeStore, ITransientDependency { var client = _cliHttpClientFactory.CreateClient(timeout: TimeSpan.FromMinutes(5)); - if (input.TemplateSource.IsNullOrWhiteSpace()) + if (isAbpIoDownload) { responseMessage = await client.PostAsync( url, @@ -334,24 +359,38 @@ public class AbpIoSourceCodeStore : ISourceCodeStore, ITransientDependency _cliHttpClientFactory.GetCancellationToken()); } - await RemoteServiceExceptionHandler.EnsureSuccessfulHttpResponseAsync(responseMessage); - var resultAsBytes = await responseMessage.Content.ReadAsByteArrayAsync(); - responseMessage.Dispose(); + if (isAbpIoDownload) + { + await EnsureAbpIoSuccessfulResponseAsync(responseMessage); + } + else + { + await RemoteServiceExceptionHandler.EnsureSuccessfulHttpResponseAsync(responseMessage); + } - return resultAsBytes; + return await responseMessage.Content.ReadAsByteArrayAsync(); } catch (Exception ex) { - if(ex is UserFriendlyException) + if (ex is CliUsageException) + { + throw; + } + + if (ex is UserFriendlyException) { Logger.LogWarning(ex.Message); throw; } - Console.WriteLine("Error occured while downloading source-code from {0} : {1}{2}{3}", url, + Console.WriteLine("Error occurred while downloading source-code from {0} : {1}{2}{3}", downloadUrl, responseMessage?.ToString(), Environment.NewLine, ex.Message); throw; } + finally + { + responseMessage?.Dispose(); + } } private static bool IsNetworkSource(string source) diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/RemoteServiceExceptionHandler.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/RemoteServiceExceptionHandler.cs index fffdb6291f..2cf1e5f6ab 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/RemoteServiceExceptionHandler.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/RemoteServiceExceptionHandler.cs @@ -4,10 +4,11 @@ using System.Linq; using System.Net.Http; using System.Text; using System.Threading.Tasks; -using Newtonsoft.Json; using Volo.Abp.DependencyInjection; using Volo.Abp.Http; using Volo.Abp.Json; +using NewtonsoftJsonException = Newtonsoft.Json.JsonException; +using SystemJsonException = System.Text.Json.JsonException; namespace Volo.Abp.Cli.ProjectBuilding; @@ -49,12 +50,11 @@ public class RemoteServiceExceptionHandler : IRemoteServiceExceptionHandler, ITr RemoteServiceErrorResponse errorResult; try { - errorResult = _jsonSerializer.Deserialize - ( + errorResult = _jsonSerializer.Deserialize( await responseMessage.Content.ReadAsStringAsync() ); } - catch (JsonReaderException) + catch (Exception ex) when (IsJsonException(ex)) { return null; } @@ -98,4 +98,9 @@ public class RemoteServiceExceptionHandler : IRemoteServiceExceptionHandler, ITr return sbError.ToString(); } + + private static bool IsJsonException(Exception ex) + { + return ex is SystemJsonException or NewtonsoftJsonException; + } } diff --git a/framework/test/Volo.Abp.Cli.Core.Tests/Volo/Abp/Cli/ProjectBuilding/RemoteServiceExceptionHandler_Tests.cs b/framework/test/Volo.Abp.Cli.Core.Tests/Volo/Abp/Cli/ProjectBuilding/RemoteServiceExceptionHandler_Tests.cs new file mode 100644 index 0000000000..32e60e9605 --- /dev/null +++ b/framework/test/Volo.Abp.Cli.Core.Tests/Volo/Abp/Cli/ProjectBuilding/RemoteServiceExceptionHandler_Tests.cs @@ -0,0 +1,193 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Shouldly; +using Volo.Abp.Cli.ProjectBuilding; +using Volo.Abp.Json; +using Volo.Abp.Json.SystemTextJson; +using Xunit; + +namespace Volo.Abp.Cli.ProjectBuilding; + +public class RemoteServiceExceptionHandler_Tests +{ + private readonly RemoteServiceExceptionHandler _handler; + + public RemoteServiceExceptionHandler_Tests() + { + var jsonSerializer = new AbpSystemTextJsonSerializer( + Microsoft.Extensions.Options.Options.Create(new AbpSystemTextJsonSerializerOptions()) + ); + _handler = new RemoteServiceExceptionHandler(jsonSerializer); + } + + [Fact] + public async Task EnsureSuccessfulHttpResponseAsync_Should_Not_Throw_On_Success() + { + var response = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("{}") + }; + + await _handler.EnsureSuccessfulHttpResponseAsync(response); + } + + [Fact] + public async Task EnsureSuccessfulHttpResponseAsync_Should_Not_Throw_When_Response_Is_Null() + { + await _handler.EnsureSuccessfulHttpResponseAsync(null); + } + + [Fact] + public async Task Should_Wrap_Html_Body_Without_Json_Parse_Exception() + { + var response = new HttpResponseMessage(HttpStatusCode.Forbidden) + { + ReasonPhrase = "Forbidden", + Content = new StringContent("Forbidden", System.Text.Encoding.UTF8, "text/html") + }; + + var exception = await Should.ThrowAsync(() => _handler.EnsureSuccessfulHttpResponseAsync(response)); + + exception.Message.ShouldContain("403-Forbidden"); + exception.Message.ShouldNotContain("invalid start of a value"); + } + + [Fact] + public async Task Should_Surface_Server_Error_Message_When_Body_Is_Valid_Json() + { + var response = new HttpResponseMessage(HttpStatusCode.Forbidden) + { + ReasonPhrase = "Forbidden", + Content = new StringContent( + "{\"error\":{\"code\":\"LicenseExpired\",\"message\":\"Your ABP license has expired.\"}}", + System.Text.Encoding.UTF8, + "application/json") + }; + + var exception = await Should.ThrowAsync(() => _handler.EnsureSuccessfulHttpResponseAsync(response)); + + exception.Message.ShouldContain("403-Forbidden"); + exception.Message.ShouldContain("LicenseExpired"); + exception.Message.ShouldContain("Your ABP license has expired."); + } + + [Fact] + public async Task Should_Surface_Server_Error_Message_For_5xx_With_Json_Body() + { + var response = new HttpResponseMessage(HttpStatusCode.InternalServerError) + { + ReasonPhrase = "Internal Server Error", + Content = new StringContent( + "{\"error\":{\"code\":\"InternalError\",\"message\":\"Database connection failed\"}}", + System.Text.Encoding.UTF8, + "application/json") + }; + + var exception = await Should.ThrowAsync(() => _handler.EnsureSuccessfulHttpResponseAsync(response)); + + exception.Message.ShouldContain("500-Internal Server Error"); + exception.Message.ShouldContain("InternalError"); + exception.Message.ShouldContain("Database connection failed"); + } + + [Fact] + public async Task GetAbpRemoteServiceErrorAsync_Should_Propagate_OperationCanceledException() + { + var response = new HttpResponseMessage(HttpStatusCode.Forbidden) + { + Content = new CanceledStringContent() + }; + + await Should.ThrowAsync( + () => _handler.GetAbpRemoteServiceErrorAsync(response) + ); + } + + [Fact] + public async Task GetAbpRemoteServiceErrorAsync_Should_Return_Null_For_Html_Body() + { + var response = new HttpResponseMessage(HttpStatusCode.Forbidden) + { + Content = new StringContent("", System.Text.Encoding.UTF8, "text/html") + }; + + var result = await _handler.GetAbpRemoteServiceErrorAsync(response); + + result.ShouldBeNull(); + } + + [Fact] + public async Task GetAbpRemoteServiceErrorAsync_Should_Return_Null_For_Newtonsoft_JsonException() + { + var handler = new RemoteServiceExceptionHandler( + new ThrowingJsonSerializer(new Newtonsoft.Json.JsonException("Invalid JSON")) + ); + var response = new HttpResponseMessage(HttpStatusCode.Forbidden) + { + Content = new StringContent("{}") + }; + + var result = await handler.GetAbpRemoteServiceErrorAsync(response); + + result.ShouldBeNull(); + } + + [Fact] + public async Task GetAbpRemoteServiceErrorAsync_Should_Propagate_Non_Json_Exceptions() + { + var handler = new RemoteServiceExceptionHandler( + new ThrowingJsonSerializer(new InvalidOperationException("Unexpected serializer failure")) + ); + var response = new HttpResponseMessage(HttpStatusCode.Forbidden) + { + Content = new StringContent("{}") + }; + + var exception = await Should.ThrowAsync( + () => handler.GetAbpRemoteServiceErrorAsync(response) + ); + + exception.Message.ShouldBe("Unexpected serializer failure"); + } + + private class CanceledStringContent : HttpContent + { + protected override Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext context) + { + throw new OperationCanceledException(); + } + + protected override bool TryComputeLength(out long length) + { + length = 0; + return false; + } + } + + private class ThrowingJsonSerializer : IJsonSerializer + { + private readonly Exception _exception; + + public ThrowingJsonSerializer(Exception exception) + { + _exception = exception; + } + + public string Serialize(object obj, bool camelCase = true, bool indented = false) + { + throw new NotImplementedException(); + } + + public T Deserialize(string jsonString, bool camelCase = true) + { + throw _exception; + } + + public object Deserialize(Type type, string jsonString, bool camelCase = true) + { + throw _exception; + } + } +}