Skip to content

Commit 521f6c9

Browse files
authored
Merge pull request #25443 from abpframework/maliming/fix-25403-cli-auth-error
Improve CLI error reporting on abp.io auth/license failure
2 parents bc0d363 + e3ccbb6 commit 521f6c9

3 files changed

Lines changed: 253 additions & 16 deletions

File tree

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

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections.Generic;
66
using System.IO;
77
using System.Linq;
8+
using System.Net;
89
using System.Net.Http;
910
using System.Text;
1011
using System.Text.RegularExpressions;
@@ -240,14 +241,14 @@ private async Task<string> GetLatestSourceCodeVersionAsync(string name, string t
240241
using (var response = await client.PostAsync(url, stringContent,
241242
_cliHttpClientFactory.GetCancellationToken(TimeSpan.FromMinutes(10))))
242243
{
243-
await RemoteServiceExceptionHandler.EnsureSuccessfulHttpResponseAsync(response);
244+
await EnsureAbpIoSuccessfulResponseAsync(response);
244245
var result = await response.Content.ReadAsStringAsync();
245246
return JsonSerializer.Deserialize<GetVersionResultDto>(result).Version;
246247
}
247248
}
248-
catch (Exception ex)
249+
catch (Exception ex) when (ex is not CliUsageException)
249250
{
250-
Console.WriteLine("Error occured while getting the latest version from {0} : {1}", url, ex.Message);
251+
Console.WriteLine("Error occurred while getting the latest version from {0} : {1}", url, ex.Message);
251252
return null;
252253
}
253254
}
@@ -273,17 +274,39 @@ private async Task<string> GetTemplateNugetVersionAsync(string name, string type
273274
using (var response = await client.PostAsync(url, stringContent,
274275
_cliHttpClientFactory.GetCancellationToken(TimeSpan.FromMinutes(10))))
275276
{
276-
await RemoteServiceExceptionHandler.EnsureSuccessfulHttpResponseAsync(response);
277+
await EnsureAbpIoSuccessfulResponseAsync(response);
277278
var result = await response.Content.ReadAsStringAsync();
278279
return JsonSerializer.Deserialize<GetVersionResultDto>(result).Version;
279280
}
280281
}
281-
catch (Exception)
282+
catch (Exception ex) when (ex is not CliUsageException)
282283
{
283284
return null;
284285
}
285286
}
286287

288+
private async Task EnsureAbpIoSuccessfulResponseAsync(HttpResponseMessage responseMessage)
289+
{
290+
if (responseMessage is { StatusCode: HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden })
291+
{
292+
var message = $"Remote server returns '{(int)responseMessage.StatusCode}-{responseMessage.ReasonPhrase}'. ";
293+
294+
var serverError = await RemoteServiceExceptionHandler.GetAbpRemoteServiceErrorAsync(responseMessage);
295+
if (!string.IsNullOrWhiteSpace(serverError))
296+
{
297+
message += serverError + " ";
298+
}
299+
300+
message += $"Authentication or license check failed while accessing {CliUrls.WwwAbpIo}. " +
301+
"Please make sure you are logged in with `abp login <username>` and your ABP commercial license is active and covers the requested version. " +
302+
$"You can check your license at {CliUrls.WwwAbpIo}my-organizations";
303+
304+
throw new CliUsageException(message);
305+
}
306+
307+
await RemoteServiceExceptionHandler.EnsureSuccessfulHttpResponseAsync(responseMessage);
308+
}
309+
287310
private async Task<bool> IsVersionExists(string templateName, string version)
288311
{
289312
var url = $"{CliUrls.WwwAbpIo}api/download/all-versions?includePreReleases=true";
@@ -313,14 +336,16 @@ private async Task<bool> IsVersionExists(string templateName, string version)
313336
private async Task<byte[]> DownloadSourceCodeContentAsync(SourceCodeDownloadInputDto input)
314337
{
315338
var url = $"{CliUrls.WwwAbpIo}api/download/{input.Type}/";
339+
var isAbpIoDownload = input.TemplateSource.IsNullOrWhiteSpace();
340+
var downloadUrl = isAbpIoDownload ? url : input.TemplateSource;
316341

317342
HttpResponseMessage responseMessage = null;
318343

319344
try
320345
{
321346
var client = _cliHttpClientFactory.CreateClient(timeout: TimeSpan.FromMinutes(5));
322347

323-
if (input.TemplateSource.IsNullOrWhiteSpace())
348+
if (isAbpIoDownload)
324349
{
325350
responseMessage = await client.PostAsync(
326351
url,
@@ -334,24 +359,38 @@ private async Task<byte[]> DownloadSourceCodeContentAsync(SourceCodeDownloadInpu
334359
_cliHttpClientFactory.GetCancellationToken());
335360
}
336361

337-
await RemoteServiceExceptionHandler.EnsureSuccessfulHttpResponseAsync(responseMessage);
338-
var resultAsBytes = await responseMessage.Content.ReadAsByteArrayAsync();
339-
responseMessage.Dispose();
362+
if (isAbpIoDownload)
363+
{
364+
await EnsureAbpIoSuccessfulResponseAsync(responseMessage);
365+
}
366+
else
367+
{
368+
await RemoteServiceExceptionHandler.EnsureSuccessfulHttpResponseAsync(responseMessage);
369+
}
340370

341-
return resultAsBytes;
371+
return await responseMessage.Content.ReadAsByteArrayAsync();
342372
}
343373
catch (Exception ex)
344374
{
345-
if(ex is UserFriendlyException)
375+
if (ex is CliUsageException)
376+
{
377+
throw;
378+
}
379+
380+
if (ex is UserFriendlyException)
346381
{
347382
Logger.LogWarning(ex.Message);
348383
throw;
349384
}
350385

351-
Console.WriteLine("Error occured while downloading source-code from {0} : {1}{2}{3}", url,
386+
Console.WriteLine("Error occurred while downloading source-code from {0} : {1}{2}{3}", downloadUrl,
352387
responseMessage?.ToString(), Environment.NewLine, ex.Message);
353388
throw;
354389
}
390+
finally
391+
{
392+
responseMessage?.Dispose();
393+
}
355394
}
356395

357396
private static bool IsNetworkSource(string source)

framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/RemoteServiceExceptionHandler.cs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@
44
using System.Net.Http;
55
using System.Text;
66
using System.Threading.Tasks;
7-
using Newtonsoft.Json;
87
using Volo.Abp.DependencyInjection;
98
using Volo.Abp.Http;
109
using Volo.Abp.Json;
10+
using NewtonsoftJsonException = Newtonsoft.Json.JsonException;
11+
using SystemJsonException = System.Text.Json.JsonException;
1112

1213
namespace Volo.Abp.Cli.ProjectBuilding;
1314

@@ -49,12 +50,11 @@ public async Task<string> GetAbpRemoteServiceErrorAsync(HttpResponseMessage resp
4950
RemoteServiceErrorResponse errorResult;
5051
try
5152
{
52-
errorResult = _jsonSerializer.Deserialize<RemoteServiceErrorResponse>
53-
(
53+
errorResult = _jsonSerializer.Deserialize<RemoteServiceErrorResponse>(
5454
await responseMessage.Content.ReadAsStringAsync()
5555
);
5656
}
57-
catch (JsonReaderException)
57+
catch (Exception ex) when (IsJsonException(ex))
5858
{
5959
return null;
6060
}
@@ -98,4 +98,9 @@ await responseMessage.Content.ReadAsStringAsync()
9898

9999
return sbError.ToString();
100100
}
101+
102+
private static bool IsJsonException(Exception ex)
103+
{
104+
return ex is SystemJsonException or NewtonsoftJsonException;
105+
}
101106
}
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
using System;
2+
using System.Net;
3+
using System.Net.Http;
4+
using System.Threading.Tasks;
5+
using Shouldly;
6+
using Volo.Abp.Cli.ProjectBuilding;
7+
using Volo.Abp.Json;
8+
using Volo.Abp.Json.SystemTextJson;
9+
using Xunit;
10+
11+
namespace Volo.Abp.Cli.ProjectBuilding;
12+
13+
public class RemoteServiceExceptionHandler_Tests
14+
{
15+
private readonly RemoteServiceExceptionHandler _handler;
16+
17+
public RemoteServiceExceptionHandler_Tests()
18+
{
19+
var jsonSerializer = new AbpSystemTextJsonSerializer(
20+
Microsoft.Extensions.Options.Options.Create(new AbpSystemTextJsonSerializerOptions())
21+
);
22+
_handler = new RemoteServiceExceptionHandler(jsonSerializer);
23+
}
24+
25+
[Fact]
26+
public async Task EnsureSuccessfulHttpResponseAsync_Should_Not_Throw_On_Success()
27+
{
28+
var response = new HttpResponseMessage(HttpStatusCode.OK)
29+
{
30+
Content = new StringContent("{}")
31+
};
32+
33+
await _handler.EnsureSuccessfulHttpResponseAsync(response);
34+
}
35+
36+
[Fact]
37+
public async Task EnsureSuccessfulHttpResponseAsync_Should_Not_Throw_When_Response_Is_Null()
38+
{
39+
await _handler.EnsureSuccessfulHttpResponseAsync(null);
40+
}
41+
42+
[Fact]
43+
public async Task Should_Wrap_Html_Body_Without_Json_Parse_Exception()
44+
{
45+
var response = new HttpResponseMessage(HttpStatusCode.Forbidden)
46+
{
47+
ReasonPhrase = "Forbidden",
48+
Content = new StringContent("<!DOCTYPE html><html><body>Forbidden</body></html>", System.Text.Encoding.UTF8, "text/html")
49+
};
50+
51+
var exception = await Should.ThrowAsync<Exception>(() => _handler.EnsureSuccessfulHttpResponseAsync(response));
52+
53+
exception.Message.ShouldContain("403-Forbidden");
54+
exception.Message.ShouldNotContain("invalid start of a value");
55+
}
56+
57+
[Fact]
58+
public async Task Should_Surface_Server_Error_Message_When_Body_Is_Valid_Json()
59+
{
60+
var response = new HttpResponseMessage(HttpStatusCode.Forbidden)
61+
{
62+
ReasonPhrase = "Forbidden",
63+
Content = new StringContent(
64+
"{\"error\":{\"code\":\"LicenseExpired\",\"message\":\"Your ABP license has expired.\"}}",
65+
System.Text.Encoding.UTF8,
66+
"application/json")
67+
};
68+
69+
var exception = await Should.ThrowAsync<Exception>(() => _handler.EnsureSuccessfulHttpResponseAsync(response));
70+
71+
exception.Message.ShouldContain("403-Forbidden");
72+
exception.Message.ShouldContain("LicenseExpired");
73+
exception.Message.ShouldContain("Your ABP license has expired.");
74+
}
75+
76+
[Fact]
77+
public async Task Should_Surface_Server_Error_Message_For_5xx_With_Json_Body()
78+
{
79+
var response = new HttpResponseMessage(HttpStatusCode.InternalServerError)
80+
{
81+
ReasonPhrase = "Internal Server Error",
82+
Content = new StringContent(
83+
"{\"error\":{\"code\":\"InternalError\",\"message\":\"Database connection failed\"}}",
84+
System.Text.Encoding.UTF8,
85+
"application/json")
86+
};
87+
88+
var exception = await Should.ThrowAsync<Exception>(() => _handler.EnsureSuccessfulHttpResponseAsync(response));
89+
90+
exception.Message.ShouldContain("500-Internal Server Error");
91+
exception.Message.ShouldContain("InternalError");
92+
exception.Message.ShouldContain("Database connection failed");
93+
}
94+
95+
[Fact]
96+
public async Task GetAbpRemoteServiceErrorAsync_Should_Propagate_OperationCanceledException()
97+
{
98+
var response = new HttpResponseMessage(HttpStatusCode.Forbidden)
99+
{
100+
Content = new CanceledStringContent()
101+
};
102+
103+
await Should.ThrowAsync<OperationCanceledException>(
104+
() => _handler.GetAbpRemoteServiceErrorAsync(response)
105+
);
106+
}
107+
108+
[Fact]
109+
public async Task GetAbpRemoteServiceErrorAsync_Should_Return_Null_For_Html_Body()
110+
{
111+
var response = new HttpResponseMessage(HttpStatusCode.Forbidden)
112+
{
113+
Content = new StringContent("<!DOCTYPE html><html></html>", System.Text.Encoding.UTF8, "text/html")
114+
};
115+
116+
var result = await _handler.GetAbpRemoteServiceErrorAsync(response);
117+
118+
result.ShouldBeNull();
119+
}
120+
121+
[Fact]
122+
public async Task GetAbpRemoteServiceErrorAsync_Should_Return_Null_For_Newtonsoft_JsonException()
123+
{
124+
var handler = new RemoteServiceExceptionHandler(
125+
new ThrowingJsonSerializer(new Newtonsoft.Json.JsonException("Invalid JSON"))
126+
);
127+
var response = new HttpResponseMessage(HttpStatusCode.Forbidden)
128+
{
129+
Content = new StringContent("{}")
130+
};
131+
132+
var result = await handler.GetAbpRemoteServiceErrorAsync(response);
133+
134+
result.ShouldBeNull();
135+
}
136+
137+
[Fact]
138+
public async Task GetAbpRemoteServiceErrorAsync_Should_Propagate_Non_Json_Exceptions()
139+
{
140+
var handler = new RemoteServiceExceptionHandler(
141+
new ThrowingJsonSerializer(new InvalidOperationException("Unexpected serializer failure"))
142+
);
143+
var response = new HttpResponseMessage(HttpStatusCode.Forbidden)
144+
{
145+
Content = new StringContent("{}")
146+
};
147+
148+
var exception = await Should.ThrowAsync<InvalidOperationException>(
149+
() => handler.GetAbpRemoteServiceErrorAsync(response)
150+
);
151+
152+
exception.Message.ShouldBe("Unexpected serializer failure");
153+
}
154+
155+
private class CanceledStringContent : HttpContent
156+
{
157+
protected override Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext context)
158+
{
159+
throw new OperationCanceledException();
160+
}
161+
162+
protected override bool TryComputeLength(out long length)
163+
{
164+
length = 0;
165+
return false;
166+
}
167+
}
168+
169+
private class ThrowingJsonSerializer : IJsonSerializer
170+
{
171+
private readonly Exception _exception;
172+
173+
public ThrowingJsonSerializer(Exception exception)
174+
{
175+
_exception = exception;
176+
}
177+
178+
public string Serialize(object obj, bool camelCase = true, bool indented = false)
179+
{
180+
throw new NotImplementedException();
181+
}
182+
183+
public T Deserialize<T>(string jsonString, bool camelCase = true)
184+
{
185+
throw _exception;
186+
}
187+
188+
public object Deserialize(Type type, string jsonString, bool camelCase = true)
189+
{
190+
throw _exception;
191+
}
192+
}
193+
}

0 commit comments

Comments
 (0)