Browse Source

Merge pull request #25443 from abpframework/maliming/fix-25403-cli-auth-error

Improve CLI error reporting on abp.io auth/license failure
pull/25446/head
Enis Necipoglu 5 months ago
committed by GitHub
parent
commit
521f6c9c84
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 63
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/AbpIoSourceCodeStore.cs
  2. 13
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/RemoteServiceExceptionHandler.cs
  3. 193
      framework/test/Volo.Abp.Cli.Core.Tests/Volo/Abp/Cli/ProjectBuilding/RemoteServiceExceptionHandler_Tests.cs

63
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/AbpIoSourceCodeStore.cs

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@ -240,14 +241,14 @@ public class AbpIoSourceCodeStore : ISourceCodeStore, ITransientDependency
using (var response = await client.PostAsync(url, stringContent, using (var response = await client.PostAsync(url, stringContent,
_cliHttpClientFactory.GetCancellationToken(TimeSpan.FromMinutes(10)))) _cliHttpClientFactory.GetCancellationToken(TimeSpan.FromMinutes(10))))
{ {
await RemoteServiceExceptionHandler.EnsureSuccessfulHttpResponseAsync(response); await EnsureAbpIoSuccessfulResponseAsync(response);
var result = await response.Content.ReadAsStringAsync(); var result = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<GetVersionResultDto>(result).Version; return JsonSerializer.Deserialize<GetVersionResultDto>(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; return null;
} }
} }
@ -273,17 +274,39 @@ public class AbpIoSourceCodeStore : ISourceCodeStore, ITransientDependency
using (var response = await client.PostAsync(url, stringContent, using (var response = await client.PostAsync(url, stringContent,
_cliHttpClientFactory.GetCancellationToken(TimeSpan.FromMinutes(10)))) _cliHttpClientFactory.GetCancellationToken(TimeSpan.FromMinutes(10))))
{ {
await RemoteServiceExceptionHandler.EnsureSuccessfulHttpResponseAsync(response); await EnsureAbpIoSuccessfulResponseAsync(response);
var result = await response.Content.ReadAsStringAsync(); var result = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<GetVersionResultDto>(result).Version; return JsonSerializer.Deserialize<GetVersionResultDto>(result).Version;
} }
} }
catch (Exception) catch (Exception ex) when (ex is not CliUsageException)
{ {
return null; 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 <username>` 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<bool> IsVersionExists(string templateName, string version) private async Task<bool> IsVersionExists(string templateName, string version)
{ {
var url = $"{CliUrls.WwwAbpIo}api/download/all-versions?includePreReleases=true"; var url = $"{CliUrls.WwwAbpIo}api/download/all-versions?includePreReleases=true";
@ -313,6 +336,8 @@ public class AbpIoSourceCodeStore : ISourceCodeStore, ITransientDependency
private async Task<byte[]> DownloadSourceCodeContentAsync(SourceCodeDownloadInputDto input) private async Task<byte[]> DownloadSourceCodeContentAsync(SourceCodeDownloadInputDto input)
{ {
var url = $"{CliUrls.WwwAbpIo}api/download/{input.Type}/"; var url = $"{CliUrls.WwwAbpIo}api/download/{input.Type}/";
var isAbpIoDownload = input.TemplateSource.IsNullOrWhiteSpace();
var downloadUrl = isAbpIoDownload ? url : input.TemplateSource;
HttpResponseMessage responseMessage = null; HttpResponseMessage responseMessage = null;
@ -320,7 +345,7 @@ public class AbpIoSourceCodeStore : ISourceCodeStore, ITransientDependency
{ {
var client = _cliHttpClientFactory.CreateClient(timeout: TimeSpan.FromMinutes(5)); var client = _cliHttpClientFactory.CreateClient(timeout: TimeSpan.FromMinutes(5));
if (input.TemplateSource.IsNullOrWhiteSpace()) if (isAbpIoDownload)
{ {
responseMessage = await client.PostAsync( responseMessage = await client.PostAsync(
url, url,
@ -334,24 +359,38 @@ public class AbpIoSourceCodeStore : ISourceCodeStore, ITransientDependency
_cliHttpClientFactory.GetCancellationToken()); _cliHttpClientFactory.GetCancellationToken());
} }
await RemoteServiceExceptionHandler.EnsureSuccessfulHttpResponseAsync(responseMessage); if (isAbpIoDownload)
var resultAsBytes = await responseMessage.Content.ReadAsByteArrayAsync(); {
responseMessage.Dispose(); await EnsureAbpIoSuccessfulResponseAsync(responseMessage);
}
else
{
await RemoteServiceExceptionHandler.EnsureSuccessfulHttpResponseAsync(responseMessage);
}
return resultAsBytes; return await responseMessage.Content.ReadAsByteArrayAsync();
} }
catch (Exception ex) catch (Exception ex)
{ {
if(ex is UserFriendlyException) if (ex is CliUsageException)
{
throw;
}
if (ex is UserFriendlyException)
{ {
Logger.LogWarning(ex.Message); Logger.LogWarning(ex.Message);
throw; 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); responseMessage?.ToString(), Environment.NewLine, ex.Message);
throw; throw;
} }
finally
{
responseMessage?.Dispose();
}
} }
private static bool IsNetworkSource(string source) private static bool IsNetworkSource(string source)

13
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.Net.Http;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Newtonsoft.Json;
using Volo.Abp.DependencyInjection; using Volo.Abp.DependencyInjection;
using Volo.Abp.Http; using Volo.Abp.Http;
using Volo.Abp.Json; using Volo.Abp.Json;
using NewtonsoftJsonException = Newtonsoft.Json.JsonException;
using SystemJsonException = System.Text.Json.JsonException;
namespace Volo.Abp.Cli.ProjectBuilding; namespace Volo.Abp.Cli.ProjectBuilding;
@ -49,12 +50,11 @@ public class RemoteServiceExceptionHandler : IRemoteServiceExceptionHandler, ITr
RemoteServiceErrorResponse errorResult; RemoteServiceErrorResponse errorResult;
try try
{ {
errorResult = _jsonSerializer.Deserialize<RemoteServiceErrorResponse> errorResult = _jsonSerializer.Deserialize<RemoteServiceErrorResponse>(
(
await responseMessage.Content.ReadAsStringAsync() await responseMessage.Content.ReadAsStringAsync()
); );
} }
catch (JsonReaderException) catch (Exception ex) when (IsJsonException(ex))
{ {
return null; return null;
} }
@ -98,4 +98,9 @@ public class RemoteServiceExceptionHandler : IRemoteServiceExceptionHandler, ITr
return sbError.ToString(); return sbError.ToString();
} }
private static bool IsJsonException(Exception ex)
{
return ex is SystemJsonException or NewtonsoftJsonException;
}
} }

193
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("<!DOCTYPE html><html><body>Forbidden</body></html>", System.Text.Encoding.UTF8, "text/html")
};
var exception = await Should.ThrowAsync<Exception>(() => _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<Exception>(() => _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<Exception>(() => _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<OperationCanceledException>(
() => _handler.GetAbpRemoteServiceErrorAsync(response)
);
}
[Fact]
public async Task GetAbpRemoteServiceErrorAsync_Should_Return_Null_For_Html_Body()
{
var response = new HttpResponseMessage(HttpStatusCode.Forbidden)
{
Content = new StringContent("<!DOCTYPE html><html></html>", 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<InvalidOperationException>(
() => 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<T>(string jsonString, bool camelCase = true)
{
throw _exception;
}
public object Deserialize(Type type, string jsonString, bool camelCase = true)
{
throw _exception;
}
}
}
Loading…
Cancel
Save