Browse Source

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
pull/25443/head
maliming 2 days ago
parent
commit
f1edf9dce8
No known key found for this signature in database GPG Key ID: A646B9CB645ECEA4
  1. 33
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/AbpIoSourceCodeStore.cs
  2. 6
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/RemoteServiceExceptionHandler.cs
  3. 135
      framework/test/Volo.Abp.Cli.Core.Tests/Volo/Abp/Cli/ProjectBuilding/RemoteServiceExceptionHandler_Tests.cs

33
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<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);
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<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 })
{
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 <username>` 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<bool> 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;

6
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<RemoteServiceErrorResponse>
(
errorResult = _jsonSerializer.Deserialize<RemoteServiceErrorResponse>(
await responseMessage.Content.ReadAsStringAsync()
);
}
catch (JsonReaderException)
catch (Exception ex) when (ex is not OperationCanceledException)
{
return null;
}

135
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("<!DOCTYPE html><html><body>Forbidden</body></html>", System.Text.Encoding.UTF8, "text/html")
};
var exception = await Should.ThrowAsync<Exception>(() => _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<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();
}
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;
}
}
}
Loading…
Cancel
Save