From f1edf9dce8a7ca486ea3fda1de6a73e8f7bf703f Mon Sep 17 00:00:00 2001 From: maliming Date: Fri, 15 May 2026 16:58:14 +0800 Subject: [PATCH 1/5] Improve CLI error reporting on abp.io auth/license failure - RemoteServiceExceptionHandler: catch all non-cancellation exceptions during error response deserialization (was only catching Newtonsoft JsonReaderException, but the active serializer throws System.Text.Json JsonException, leaking '<' is an invalid start of a value to users) - AbpIoSourceCodeStore: wrap 401/403 responses from abp.io endpoints in CliUsageException with login + license hint, so users get an actionable message instead of HTML-as-JSON parse failure - Add unit tests for RemoteServiceExceptionHandler covering HTML body, valid JSON error, 5xx, and OperationCanceledException propagation --- .../ProjectBuilding/AbpIoSourceCodeStore.cs | 33 ++++- .../RemoteServiceExceptionHandler.cs | 6 +- .../RemoteServiceExceptionHandler_Tests.cs | 135 ++++++++++++++++++ 3 files changed, 164 insertions(+), 10 deletions(-) create mode 100644 framework/test/Volo.Abp.Cli.Core.Tests/Volo/Abp/Cli/ProjectBuilding/RemoteServiceExceptionHandler_Tests.cs 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..86ad1f5013 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,12 +241,12 @@ 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); return null; @@ -273,17 +274,32 @@ 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 }) + { + throw new CliUsageException( + $"Remote server returns '{(int)responseMessage.StatusCode}-{responseMessage.ReasonPhrase}'. " + + "Authentication or license check failed while accessing abp.io. " + + "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 https://abp.io/my-organizations" + ); + } + + await RemoteServiceExceptionHandler.EnsureSuccessfulHttpResponseAsync(responseMessage); + } + private async Task IsVersionExists(string templateName, string version) { var url = $"{CliUrls.WwwAbpIo}api/download/all-versions?includePreReleases=true"; @@ -334,7 +350,7 @@ public class AbpIoSourceCodeStore : ISourceCodeStore, ITransientDependency _cliHttpClientFactory.GetCancellationToken()); } - await RemoteServiceExceptionHandler.EnsureSuccessfulHttpResponseAsync(responseMessage); + await EnsureAbpIoSuccessfulResponseAsync(responseMessage); var resultAsBytes = await responseMessage.Content.ReadAsByteArrayAsync(); responseMessage.Dispose(); @@ -342,7 +358,12 @@ public class AbpIoSourceCodeStore : ISourceCodeStore, ITransientDependency } catch (Exception ex) { - if(ex is UserFriendlyException) + if (ex is CliUsageException) + { + throw; + } + + if (ex is UserFriendlyException) { Logger.LogWarning(ex.Message); throw; 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..41150ec16e 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,7 +4,6 @@ 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; @@ -49,12 +48,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 (ex is not OperationCanceledException) { return null; } 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..85193d3f90 --- /dev/null +++ b/framework/test/Volo.Abp.Cli.Core.Tests/Volo/Abp/Cli/ProjectBuilding/RemoteServiceExceptionHandler_Tests.cs @@ -0,0 +1,135 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Shouldly; +using Volo.Abp.Cli.ProjectBuilding; +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.GetType().ShouldBe(typeof(Exception)); + 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(); + } + + 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; + } + } +} From 884768448e58f773d5f3f8afa06d2901c1d28582 Mon Sep 17 00:00:00 2001 From: maliming Date: Fri, 15 May 2026 17:26:39 +0800 Subject: [PATCH 2/5] Address Copilot review feedback - Drop manual HttpResponseMessage dispose in DownloadSourceCodeContentAsync, consistent with Volo.Abp.Http.Client.ClientProxyBase and ASP.NET Core OAuth handlers (default HttpCompletionOption.ResponseContentRead does not need manual disposal) - Fix "occured" -> "occurred" typos in two log lines - Remove unused using Microsoft.Extensions.Options from test --- .../Volo/Abp/Cli/ProjectBuilding/AbpIoSourceCodeStore.cs | 9 +++------ .../RemoteServiceExceptionHandler_Tests.cs | 1 - 2 files changed, 3 insertions(+), 7 deletions(-) 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 86ad1f5013..29c9bf8938 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 @@ -248,7 +248,7 @@ public class AbpIoSourceCodeStore : ISourceCodeStore, ITransientDependency } 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; } } @@ -351,10 +351,7 @@ public class AbpIoSourceCodeStore : ISourceCodeStore, ITransientDependency } await EnsureAbpIoSuccessfulResponseAsync(responseMessage); - var resultAsBytes = await responseMessage.Content.ReadAsByteArrayAsync(); - responseMessage.Dispose(); - - return resultAsBytes; + return await responseMessage.Content.ReadAsByteArrayAsync(); } catch (Exception ex) { @@ -369,7 +366,7 @@ public class AbpIoSourceCodeStore : ISourceCodeStore, ITransientDependency 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}", url, responseMessage?.ToString(), Environment.NewLine, ex.Message); throw; } 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 index 85193d3f90..a5730019de 100644 --- 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 @@ -2,7 +2,6 @@ using System; using System.Net; using System.Net.Http; using System.Threading.Tasks; -using Microsoft.Extensions.Options; using Shouldly; using Volo.Abp.Cli.ProjectBuilding; using Volo.Abp.Json.SystemTextJson; From d3ab33a82d26841d9abe331de752dea3a666828e Mon Sep 17 00:00:00 2001 From: maliming Date: Fri, 15 May 2026 17:40:44 +0800 Subject: [PATCH 3/5] Drop brittle exception type assertion in RemoteServiceExceptionHandler test Asserting GetType() == typeof(Exception) locks the test to the base type. The remaining message assertions already cover the intent of ensuring the underlying JSON parse error does not surface. --- .../Cli/ProjectBuilding/RemoteServiceExceptionHandler_Tests.cs | 1 - 1 file changed, 1 deletion(-) 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 index a5730019de..7f41db9e3a 100644 --- 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 @@ -49,7 +49,6 @@ public class RemoteServiceExceptionHandler_Tests var exception = await Should.ThrowAsync(() => _handler.EnsureSuccessfulHttpResponseAsync(response)); - exception.GetType().ShouldBe(typeof(Exception)); exception.Message.ShouldContain("403-Forbidden"); exception.Message.ShouldNotContain("invalid start of a value"); } From 9ddc15114f2efa0e996f12f287d5ca7dd41cb842 Mon Sep 17 00:00:00 2001 From: maliming Date: Fri, 15 May 2026 18:33:47 +0800 Subject: [PATCH 4/5] Address Copilot review feedback (round 2) - Narrow GetAbpRemoteServiceErrorAsync catch to JSON deserialization exceptions (System.Text.Json + Newtonsoft) so OOM and other runtime errors are no longer swallowed - Dispose HttpResponseMessage in DownloadSourceCodeContentAsync via finally block, matching the pattern used by sibling methods - Use generic EnsureSuccessfulHttpResponseAsync for user-supplied TemplateSource downloads so non-abp.io 401/403 responses don't show a misleading abp.io license hint - Add tests for Newtonsoft JsonException handling and non-JSON exception propagation --- .../ProjectBuilding/AbpIoSourceCodeStore.cs | 20 ++++++- .../RemoteServiceExceptionHandler.cs | 9 ++- .../RemoteServiceExceptionHandler_Tests.cs | 60 +++++++++++++++++++ 3 files changed, 85 insertions(+), 4 deletions(-) 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 29c9bf8938..3ee8c5c7f3 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 @@ -329,6 +329,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; @@ -336,7 +338,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, @@ -350,7 +352,15 @@ public class AbpIoSourceCodeStore : ISourceCodeStore, ITransientDependency _cliHttpClientFactory.GetCancellationToken()); } - await EnsureAbpIoSuccessfulResponseAsync(responseMessage); + if (isAbpIoDownload) + { + await EnsureAbpIoSuccessfulResponseAsync(responseMessage); + } + else + { + await RemoteServiceExceptionHandler.EnsureSuccessfulHttpResponseAsync(responseMessage); + } + return await responseMessage.Content.ReadAsByteArrayAsync(); } catch (Exception ex) @@ -366,10 +376,14 @@ public class AbpIoSourceCodeStore : ISourceCodeStore, ITransientDependency throw; } - Console.WriteLine("Error occurred 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 41150ec16e..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 @@ -7,6 +7,8 @@ using System.Threading.Tasks; 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; @@ -52,7 +54,7 @@ public class RemoteServiceExceptionHandler : IRemoteServiceExceptionHandler, ITr await responseMessage.Content.ReadAsStringAsync() ); } - catch (Exception ex) when (ex is not OperationCanceledException) + catch (Exception ex) when (IsJsonException(ex)) { return null; } @@ -96,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 index 7f41db9e3a..32e60e9605 100644 --- 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 @@ -4,6 +4,7 @@ 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; @@ -117,6 +118,40 @@ public class RemoteServiceExceptionHandler_Tests 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) @@ -130,4 +165,29 @@ public class RemoteServiceExceptionHandler_Tests 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; + } + } } From e3ccbb6fbef01609f6baebf8a56f7b4bc8c51b5e Mon Sep 17 00:00:00 2001 From: maliming Date: Sat, 16 May 2026 11:37:33 +0800 Subject: [PATCH 5/5] Address Copilot review feedback (round 3) - Use CliUrls.WwwAbpIo instead of hardcoded abp.io host/URL in the 401/403 license hint, so dev/staging environments show the right URL - Include server-provided RemoteServiceErrorResponse details (e.g. Code: LicenseExpired) in the CliUsageException message when available --- .../ProjectBuilding/AbpIoSourceCodeStore.cs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) 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 3ee8c5c7f3..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 @@ -289,12 +289,19 @@ public class AbpIoSourceCodeStore : ISourceCodeStore, ITransientDependency { if (responseMessage is { StatusCode: HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden }) { - throw new CliUsageException( - $"Remote server returns '{(int)responseMessage.StatusCode}-{responseMessage.ReasonPhrase}'. " + - "Authentication or license check failed while accessing abp.io. " + - "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 https://abp.io/my-organizations" - ); + 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);