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 2 weeks 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.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<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;
}
}
@ -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<GetVersionResultDto>(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 <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)
{
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)
{
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)

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.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<RemoteServiceErrorResponse>
(
errorResult = _jsonSerializer.Deserialize<RemoteServiceErrorResponse>(
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;
}
}

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