diff --git a/CHANGELOG.md b/CHANGELOG.md index f772905..402e19d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## v0.8.0-alpha +* Added the following exceptions: + - `NotFoundException` thrown from `GetEngineAsync(string engine)`, `GetDatabaseAsync(string database)`, etc. when requested resource (Engine/Database/Model/User/Client) doesn't exist or got deleted. + - `ApiException` thrown when RAI API responds with 5xx status codes or contains unsupported content type. + - `EngineProvisionFailedException` thrown from `CreateEngineWaitAsync` when requested engine failed to provision. + - `CredentialsNotSupportedException` thrown from every method when credentials to access RAI API are not provided or provided credentials are unsupported (i.e. not for OAuth Client credentials method). + - `InvalidResponseException` thrown when RAI API response has unexpected format or content type. + ## v0.7.0-alpha * Replaced String properties with Enums in the following models returned by corresponding API methods: - `Database`.`State` property is of `DatabaseState` type. diff --git a/RelationalAI.Test/DatabaseTest.cs b/RelationalAI.Test/DatabaseTest.cs index 54d90a4..13b3e73 100644 --- a/RelationalAI.Test/DatabaseTest.cs +++ b/RelationalAI.Test/DatabaseTest.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using RelationalAI.Errors; using RelationalAI.Models.Database; using Xunit; @@ -10,13 +11,14 @@ public class DatabaseTests : UnitTest public static string Uuid = Guid.NewGuid().ToString(); public static string Dbname = $"csharp-sdk-{Uuid}"; public static string EngineName = $"csharp-sdk-{Uuid}"; + [Fact] public async Task DatabaseTest() { var client = CreateClient(); await client.CreateEngineWaitAsync(EngineName); - await Assert.ThrowsAsync(async () => await client.DeleteDatabaseAsync(Dbname)); + await Assert.ThrowsAsync(async () => await client.DeleteDatabaseAsync(Dbname)); var createRsp = await client.CreateDatabaseAsync(Dbname, EngineName, false); Assert.Equal(Dbname, createRsp.Name); @@ -62,7 +64,7 @@ await Assert.ThrowsAsync(() => var deleteRsp = await client.DeleteDatabaseAsync(Dbname); Assert.Equal(Dbname, deleteRsp.Name); - await Assert.ThrowsAsync(async () => await client.GetDatabaseAsync(Dbname)); + await Assert.ThrowsAsync(async () => await client.GetDatabaseAsync(Dbname)); } private const string TestModel = "def R = \"hello\", \"world\""; @@ -79,7 +81,7 @@ public async Task DatabaseCloneTest() var client = CreateClient(); await client.CreateEngineWaitAsync(EngineName); - await Assert.ThrowsAsync(async () => await client.DeleteDatabaseAsync(Dbname)); + await Assert.ThrowsAsync(async () => await client.DeleteDatabaseAsync(Dbname)); // create a fresh database var createRsp = await client.CreateDatabaseAsync(Dbname, EngineName); @@ -151,8 +153,23 @@ public override async Task DisposeAsync() { var client = CreateClient(); - try { await client.DeleteDatabaseAsync(Dbname); } catch { } - try { await client.DeleteEngineWaitAsync(EngineName); } catch { } + try + { + await client.DeleteDatabaseAsync(Dbname); + } + catch (Exception e) + { + await Console.Error.WriteLineAsync(e.ToString()); + } + + try + { + await client.DeleteEngineWaitAsync(EngineName); + } + catch (Exception e) + { + await Console.Error.WriteLineAsync(e.ToString()); + } } } } diff --git a/RelationalAI.Test/EngineTest.cs b/RelationalAI.Test/EngineTest.cs index 73b34b6..1f4f152 100644 --- a/RelationalAI.Test/EngineTest.cs +++ b/RelationalAI.Test/EngineTest.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using RelationalAI.Errors; using RelationalAI.Models.Engine; using Xunit; @@ -34,9 +35,9 @@ public async Task EngineTest() await Assert.ThrowsAsync(() => client.ListEnginesAsync((EngineState)1500)); - await Assert.ThrowsAsync(async () => await client.DeleteEngineWaitAsync(EngineName)); + await Assert.ThrowsAsync(async () => await client.DeleteEngineWaitAsync(EngineName)); - await Assert.ThrowsAsync(async () => await client.GetEngineAsync(EngineName)); + await Assert.ThrowsAsync(async () => await client.GetEngineAsync(EngineName)); engines = await client.ListEnginesAsync(); engine = engines.Find(item => item.Name.Equals(EngineName)); @@ -47,7 +48,14 @@ public override async Task DisposeAsync() { var client = CreateClient(); - try { await client.DeleteEngineWaitAsync(EngineName); } catch { } + try + { + await client.DeleteEngineWaitAsync(EngineName); + } + catch (Exception e) + { + await Console.Error.WriteLineAsync(e.ToString()); + } } } } diff --git a/RelationalAI.Test/ExecuteAsync.cs b/RelationalAI.Test/ExecuteAsync.cs index 02eeb1f..edb47c2 100644 --- a/RelationalAI.Test/ExecuteAsync.cs +++ b/RelationalAI.Test/ExecuteAsync.cs @@ -45,8 +45,23 @@ public override async Task DisposeAsync() { var client = CreateClient(); - try { await client.DeleteDatabaseAsync(Dbname); } catch { } - try { await client.DeleteEngineWaitAsync(EngineName); } catch { } + try + { + await client.DeleteDatabaseAsync(Dbname); + } + catch (Exception e) + { + await Console.Error.WriteLineAsync(e.ToString()); + } + + try + { + await client.DeleteEngineWaitAsync(EngineName); + } + catch (Exception e) + { + await Console.Error.WriteLineAsync(e.ToString()); + } } } } diff --git a/RelationalAI.Test/ExecuteV1Test.cs b/RelationalAI.Test/ExecuteV1Test.cs index e1136ef..89d7d86 100644 --- a/RelationalAI.Test/ExecuteV1Test.cs +++ b/RelationalAI.Test/ExecuteV1Test.cs @@ -45,8 +45,23 @@ public override async Task DisposeAsync() { var client = CreateClient(); - try { await client.DeleteDatabaseAsync(Dbname); } catch { } - try { await client.DeleteEngineWaitAsync(EngineName); } catch { } + try + { + await client.DeleteDatabaseAsync(Dbname); + } + catch (Exception e) + { + await Console.Error.WriteLineAsync(e.ToString()); + } + + try + { + await client.DeleteEngineWaitAsync(EngineName); + } + catch (Exception e) + { + await Console.Error.WriteLineAsync(e.ToString()); + } } } } diff --git a/RelationalAI.Test/LoadCsvTest.cs b/RelationalAI.Test/LoadCsvTest.cs index fa5a3ff..4444fb9 100644 --- a/RelationalAI.Test/LoadCsvTest.cs +++ b/RelationalAI.Test/LoadCsvTest.cs @@ -312,8 +312,23 @@ public override async Task DisposeAsync() { var client = CreateClient(); - try { await client.DeleteDatabaseAsync(Dbname); } catch { } - try { await client.DeleteEngineWaitAsync(EngineName); } catch { } + try + { + await client.DeleteDatabaseAsync(Dbname); + } + catch (Exception e) + { + await Console.Error.WriteLineAsync(e.ToString()); + } + + try + { + await client.DeleteEngineWaitAsync(EngineName); + } + catch (Exception e) + { + await Console.Error.WriteLineAsync(e.ToString()); + } } } } diff --git a/RelationalAI.Test/LoadJsonTest.cs b/RelationalAI.Test/LoadJsonTest.cs index d7cccf5..8a8f9be 100644 --- a/RelationalAI.Test/LoadJsonTest.cs +++ b/RelationalAI.Test/LoadJsonTest.cs @@ -56,8 +56,23 @@ public override async Task DisposeAsync() { var client = CreateClient(); - try { await client.DeleteDatabaseAsync(Dbname); } catch { } - try { await client.DeleteEngineWaitAsync(EngineName); } catch { } + try + { + await client.DeleteDatabaseAsync(Dbname); + } + catch (Exception e) + { + await Console.Error.WriteLineAsync(e.ToString()); + } + + try + { + await client.DeleteEngineWaitAsync(EngineName); + } + catch (Exception e) + { + await Console.Error.WriteLineAsync(e.ToString()); + } } } } diff --git a/RelationalAI.Test/ModelsTest.cs b/RelationalAI.Test/ModelsTest.cs index 3ba2eac..a94f751 100644 --- a/RelationalAI.Test/ModelsTest.cs +++ b/RelationalAI.Test/ModelsTest.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using RelationalAI.Errors; using Xunit; namespace RelationalAI.Test @@ -39,7 +40,7 @@ public async Task ModelsTest() Assert.Empty(deleteRsp.Output); Assert.Empty(deleteRsp.Problems); - await Assert.ThrowsAsync(async () => await client.GetModelAsync(Dbname, EngineName, "test_model")); + await Assert.ThrowsAsync(async () => await client.GetModelAsync(Dbname, EngineName, "test_model")); modelNames = await client.ListModelNamesAsync(Dbname, EngineName); modelName = modelNames.Find(item => item.Equals("test_model")); @@ -54,8 +55,23 @@ public override async Task DisposeAsync() { var client = CreateClient(); - try { await client.DeleteDatabaseAsync(Dbname); } catch { } - try { await client.DeleteEngineWaitAsync(EngineName); } catch { } + try + { + await client.DeleteDatabaseAsync(Dbname); + } + catch (Exception e) + { + await Console.Error.WriteLineAsync(e.ToString()); + } + + try + { + await client.DeleteEngineWaitAsync(EngineName); + } + catch (Exception e) + { + await Console.Error.WriteLineAsync(e.ToString()); + } } } } diff --git a/RelationalAI.Test/OAuthClientTest.cs b/RelationalAI.Test/OAuthClientTest.cs index caeb8a3..736b822 100644 --- a/RelationalAI.Test/OAuthClientTest.cs +++ b/RelationalAI.Test/OAuthClientTest.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using RelationalAI.Errors; using Xunit; namespace RelationalAI.Test @@ -13,7 +14,7 @@ public async Task OAuthClientTest() { var client = CreateClient(); - await Assert.ThrowsAsync(async () => await client.FindOAuthClientAsync(OAuthClientName)); + await Assert.ThrowsAsync(async () => await client.FindOAuthClientAsync(OAuthClientName)); var rsp = await client.CreateOAuthClientAsync(OAuthClientName); Assert.Equal(OAuthClientName, rsp.Name); @@ -37,7 +38,7 @@ public async Task OAuthClientTest() var deleteRsp = await client.DeleteOAuthClientAsync(clientId); Assert.Equal(clientId, deleteRsp.Id); - await Assert.ThrowsAsync(async () => await client.FindOAuthClientAsync(OAuthClientName)); + await Assert.ThrowsAsync(async () => await client.FindOAuthClientAsync(OAuthClientName)); } public override async Task DisposeAsync() @@ -49,7 +50,10 @@ public override async Task DisposeAsync() var oauthClient = await client.FindOAuthClientAsync(OAuthClientName); await client.DeleteOAuthClientAsync(oauthClient.Id); } - catch { } + catch (Exception e) + { + await Console.Error.WriteLineAsync(e.ToString()); + } } } } diff --git a/RelationalAI.Test/UserTest.cs b/RelationalAI.Test/UserTest.cs index 489e6eb..fb88524 100644 --- a/RelationalAI.Test/UserTest.cs +++ b/RelationalAI.Test/UserTest.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using RelationalAI.Errors; using RelationalAI.Models.User; using Xunit; @@ -17,7 +18,7 @@ public async Task TestUser() { var client = CreateClient(); - await Assert.ThrowsAsync(async () => await client.FindUserAsync(UserEmail)); + await Assert.ThrowsAsync(async () => await client.FindUserAsync(UserEmail)); var rsp = await client.CreateUserAsync(UserEmail); Assert.Equal(UserEmail, rsp.Email); @@ -72,7 +73,10 @@ public override async Task DisposeAsync() var oauthClient = await client.FindUserAsync(UserEmail); await client.DeleteUserAsync(oauthClient.Id); } - catch { } + catch (Exception e) + { + await Console.Error.WriteLineAsync(e.ToString()); + } } } } diff --git a/RelationalAI/Credentials/ClientCredentials.cs b/RelationalAI/Credentials/ClientCredentials.cs index 9260d7d..a4f7a45 100644 --- a/RelationalAI/Credentials/ClientCredentials.cs +++ b/RelationalAI/Credentials/ClientCredentials.cs @@ -23,7 +23,7 @@ public class ClientCredentials : ICredentials private const string DefaultClientCredentialsUrl = "https://login.relationalai.com/oauth/token"; private string _clientId; private string _clientSecret; - private string _clientCredentialsUrl; + private string _clientCredentialsUrl = DefaultClientCredentialsUrl; public ClientCredentials(string clientId, string clientSecret) { diff --git a/RelationalAI/Errors/ApiException.cs b/RelationalAI/Errors/ApiException.cs new file mode 100644 index 0000000..624ae8b --- /dev/null +++ b/RelationalAI/Errors/ApiException.cs @@ -0,0 +1,65 @@ +/* + * Copyright 2022 RelationalAI, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using System.Net; + +namespace RelationalAI.Errors +{ + /// + /// Represents error thrown when RAI API request failed with error status code, etc. + /// + public class ApiException : Exception + { + public ApiException( + string message, + HttpStatusCode statusCode, + string response, + string requestId = null) + : base($"Request failed with error: {message}") + { + StatusCode = statusCode; + Response = response; + RequestId = requestId; + } + + /// + /// Gets the status code of the RAI API response. + /// + public HttpStatusCode StatusCode { get; } + + /// + /// Gets the raw RAI API response content. + /// + public string Response { get; } + + /// + /// Gets id of the failed request from the RAI API response. + /// + public string RequestId { get; } + + /// + /// Gets string representation of the error details, including Status Code, Headers and Response content. + /// + /// String representation of the exception. + public override string ToString() + { + return $"StatusCode: {StatusCode}, RequestId: {RequestId ?? "Unknown"}," + Environment.NewLine + + $"Response: {Response}" + Environment.NewLine + + base.ToString(); + } + } +} diff --git a/RelationalAI/Errors/CredentialsNotSupportedException.cs b/RelationalAI/Errors/CredentialsNotSupportedException.cs new file mode 100644 index 0000000..9ed2dc3 --- /dev/null +++ b/RelationalAI/Errors/CredentialsNotSupportedException.cs @@ -0,0 +1,31 @@ +/* + * Copyright 2022 RelationalAI, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; + +namespace RelationalAI.Errors +{ + /// + /// Represents error thrown when provided credentials are unsupported. + /// + public class CredentialsNotSupportedException : Exception + { + public CredentialsNotSupportedException() + : base("Provided credentials for unsupported auth method.") + { + } + } +} diff --git a/RelationalAI/Errors/EngineProvisionFailedException.cs b/RelationalAI/Errors/EngineProvisionFailedException.cs new file mode 100644 index 0000000..cc60ac4 --- /dev/null +++ b/RelationalAI/Errors/EngineProvisionFailedException.cs @@ -0,0 +1,38 @@ +/* + * Copyright 2022 RelationalAI, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using RelationalAI.Models.Engine; + +namespace RelationalAI.Errors +{ + /// + /// Represents error thrown when engine requested to provision failed to get provisioned. + /// + public class EngineProvisionFailedException : Exception + { + public EngineProvisionFailedException(Engine engine) + : base($"Engine with name `{engine.Name}` failed to provision") + { + Engine = engine; + } + + /// + /// Gets the name of the engine that failed to provision. + /// + public Engine Engine { get; } + } +} diff --git a/RelationalAI/Errors/InvalidResponseException.cs b/RelationalAI/Errors/InvalidResponseException.cs new file mode 100644 index 0000000..20d16d1 --- /dev/null +++ b/RelationalAI/Errors/InvalidResponseException.cs @@ -0,0 +1,47 @@ +/* + * Copyright 2022 RelationalAI, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; + +namespace RelationalAI.Errors +{ + /// + /// Represents error thrown when invalid / unexpectedly formatted response received from RAI API. + /// + public class InvalidResponseException : Exception + { + public InvalidResponseException(string message, object response = null) + : base(message) + { + Response = response; + } + + /// + /// Gets the response received from RAI API. + /// + public object Response { get; } + + /// + /// Gets string representation of the error details, including the Response. + /// + /// String representation of the exception. + public override string ToString() + { + return $"Response: {Response}" + Environment.NewLine + + base.ToString(); + } + } +} diff --git a/RelationalAI/Errors/NotFoundException.cs b/RelationalAI/Errors/NotFoundException.cs new file mode 100644 index 0000000..3526532 --- /dev/null +++ b/RelationalAI/Errors/NotFoundException.cs @@ -0,0 +1,31 @@ +/* + * Copyright 2022 RelationalAI, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; + +namespace RelationalAI.Errors +{ + /// + /// Represents error thrown when requested resource was not found in the system of record. + /// + public class NotFoundException : Exception + { + public NotFoundException(string message) + : base(message) + { + } + } +} diff --git a/RelationalAI/Models/User/Roles.cs b/RelationalAI/Models/User/Roles.cs index b566473..85a3faf 100644 --- a/RelationalAI/Models/User/Roles.cs +++ b/RelationalAI/Models/User/Roles.cs @@ -32,7 +32,7 @@ public static string Value(this Role role) { Role.User => "user", Role.Admin => "admin", - _ => throw new SystemException($"role '{role}' not supported") + _ => throw new ArgumentOutOfRangeException(nameof(role), role, $"Role '{role}' not supported") }; } } diff --git a/RelationalAI/RelationalAI.csproj b/RelationalAI/RelationalAI.csproj index c574fab..808629a 100644 --- a/RelationalAI/RelationalAI.csproj +++ b/RelationalAI/RelationalAI.csproj @@ -1,7 +1,7 @@  - 0.7.0-alpha + 0.8.0-alpha netcoreapp3.1 RAI RelationalAI diff --git a/RelationalAI/Services/Client.cs b/RelationalAI/Services/Client.cs index be70073..d85caf1 100644 --- a/RelationalAI/Services/Client.cs +++ b/RelationalAI/Services/Client.cs @@ -23,6 +23,7 @@ using Newtonsoft.Json.Linq; using Polly; using RelationalAI.Credentials; +using RelationalAI.Errors; using RelationalAI.Models.Database; using RelationalAI.Models.Edb; using RelationalAI.Models.Engine; @@ -86,7 +87,7 @@ public async Task GetDatabaseAsync(string database) var resp = await GetResourceAsync(PathDatabase, null, parameters); var dbs = Json.Deserialize(resp).Databases; - return dbs.Count > 0 ? dbs[0] : throw new SystemException("not found"); + return dbs.Count > 0 ? dbs[0] : throw new NotFoundException($"Database with name `{database}` not found"); } public async Task> ListDatabasesAsync(DatabaseState? state = null) @@ -133,8 +134,7 @@ public async Task CreateEngineWaitAsync(string engine, EngineSize size = if (resp.State != EngineState.Provisioned) { - // TODO: replace with a better error during introducing the exceptions hierarchy - throw new SystemException("Failed to provision engine"); + throw new EngineProvisionFailedException(resp); } return resp; @@ -149,7 +149,7 @@ public async Task GetEngineAsync(string engine) }; var resp = await GetResourceAsync(PathEngine, null, parameters); var engines = Json.Deserialize(resp).Engines; - return engines.Count > 0 ? engines[0] : throw new SystemException("not found"); + return engines.Count > 0 ? engines[0] : throw new NotFoundException($"Engine with name `{engine}` not found"); } public async Task> ListEnginesAsync(EngineState? state = null) @@ -203,7 +203,7 @@ public async Task FindOAuthClientAsync(string name) var clients = await ListOAuthClientsAsync(); return clients.FirstOrDefault(client => client.Name == name) ?? - throw new SystemException("not found"); + throw new NotFoundException($"OAuth Client with name `{name}` not found"); } public async Task GetOAuthClientAsync(string id) @@ -261,7 +261,7 @@ public async Task FindUserAsync(string email) var users = await ListUsersAsync(); return users.FirstOrDefault(user => user.Email == email) ?? - throw new SystemException("not found"); + throw new NotFoundException($"User with email `{email}` not found"); } public async Task GetUserAsync(string userId) @@ -395,7 +395,7 @@ public async Task GetModelAsync(string database, string engine, string na var models = await ListModelsAsync(database, engine); return models.FirstOrDefault(model => model.Name.Equals(name)) ?? - throw new SystemException($"model {name} not found."); + throw new NotFoundException($"Model with name `{name}` not found on database {database}"); } public async Task DeleteModelAsync(string database, string engine, string name) @@ -556,7 +556,7 @@ private static string GenLiteral(object value) switch (value) { case null: - throw new SystemException("Cannot generate literal from null value"); + throw new ArgumentException("Cannot generate literal from null value"); case Int16 _: case Int32 _: case Int64 _: @@ -564,7 +564,7 @@ private static string GenLiteral(object value) case char c: return GenLiteral(c); default: - throw new SystemException($"Cannot generate type from {value.GetType()} value"); + throw new ArgumentException($"Cannot generate literal from {value.GetType()} value"); } } @@ -644,7 +644,7 @@ private static List ParseProblemsResult(string rsp) var problems = JsonConvert.DeserializeObject(rsp); if (!(problems is JArray problemsArray)) { - throw new SystemException("Unexpected format of problems"); + throw new InvalidResponseException("Unexpected format of transaction problems", rsp); } foreach (var problem in problemsArray) @@ -654,7 +654,7 @@ private static List ParseProblemsResult(string rsp) { output.Add(Json.Deserialize(data)); } - catch (SystemException) + catch (InvalidResponseException) { output.Add(Json.Deserialize(data)); } @@ -671,12 +671,12 @@ private TransactionAsyncResult ReadTransactionAsyncResults(List.Deserialize(_rest.ReadString(transaction.Data)); @@ -705,7 +705,7 @@ private async Task GetResourceAsync(string path, string key = null, Dict // making sure there aren't more than one value if (result.First != result.Last) { - throw new SystemException("more than one resources found"); + throw new InvalidResponseException("More than one resource found", response); } result = result.First; @@ -742,7 +742,9 @@ private async Task GetStringResponseAsync(string path, Dictionary ParseMultipartResponse(byte[] content) return output; } + private static async Task EnsureSuccessResponseAsync(HttpResponseMessage response) + { + var status = (int)response.StatusCode; + if (status != 404 && status < 500) + { + return; + } + + var content = await response.Content.ReadAsStringAsync(); + if (status == 404) + { + throw new NotFoundException(content); + } + + var requestId = response.Headers.TryGetValues(RequestIdHeaderName, out var values) ? values.FirstOrDefault() : null; + throw new ApiException($"Server error {response.ReasonPhrase}", response.StatusCode, content, requestId); + } + private async Task GetAccessTokenAsync(string host) { if (!(_context.Credentials is ClientCredentials creds)) { - throw new SystemException("credential not supported"); + throw new CredentialsNotSupportedException(); } if (creds.AccessToken == null || creds.AccessToken.IsExpired) @@ -293,14 +314,16 @@ private async Task RequestAccessTokenAsync(string host, ClientCrede var resp = await RequestHelperAsync("POST", creds.ClientCredentialsUrl, data); if (!(resp is string stringResponse)) { - throw new SystemException("Unexpected response type"); + throw new InvalidResponseException( + $"Unexpected response type, expected a string but received {resp.GetType().Name}", + resp); } var result = JsonConvert.DeserializeObject>(stringResponse); if (result == null) { - throw new SystemException("Unexpected access token response format"); + throw new InvalidResponseException("Unexpected access token response format", resp); } return new AccessToken(result["access_token"], int.Parse(result["expires_in"])); @@ -323,16 +346,18 @@ private async Task RequestHelperAsync( var request = PrepareHttpRequest(method, client.BaseAddress, EncodeContent(data), headers, parameters); // Get the result back or throws an exception. - var httpResponse = await client.SendAsync(request); - var content = await httpResponse.Content.ReadAsByteArrayAsync(); - var contentType = httpResponse.Content.Headers.ContentType.MediaType; + var response = await client.SendAsync(request); + await EnsureSuccessResponseAsync(response); + var content = await response.Content.ReadAsByteArrayAsync(); + var contentType = response.Content.Headers.ContentType.MediaType; return contentType.ToLower() switch { "application/json" => ReadString(content), "application/x-protobuf" => ReadMetadataProtobuf(content), "multipart/form-data" => ParseMultipartResponse(content), - _ => throw new SystemException($"unsupported content-type: {contentType}") + _ => throw new ApiException( + $"Unsupported response content-type: {contentType}", response.StatusCode, ReadString(content)) }; } diff --git a/RelationalAI/Utils/CommonPolicies.cs b/RelationalAI/Utils/CommonPolicies.cs index 78d4c29..4914892 100644 --- a/RelationalAI/Utils/CommonPolicies.cs +++ b/RelationalAI/Utils/CommonPolicies.cs @@ -17,6 +17,7 @@ using System; using System.Net.Http; using Polly; +using RelationalAI.Errors; namespace RelationalAI.Utils { @@ -105,9 +106,8 @@ private static AsyncPolicy GetRequestErrorResiliencePolicy() // failure, server certificate validation or timeout. .Handle() - // Response deserialization failed due to unsuccessful response received (5xx status code, etc.) - // TODO: update after introducing exceptions hierarchy - .Or() + // Server error response received (5xx status code, etc.) + .Or() // Retry 5 times. In this case will wait for: 2 + 4 + 8 + 16 + 30 seconds // And rethrow the exception. diff --git a/RelationalAI/Utils/Json.cs b/RelationalAI/Utils/Json.cs index 932db8d..d812dc8 100644 --- a/RelationalAI/Utils/Json.cs +++ b/RelationalAI/Utils/Json.cs @@ -14,18 +14,19 @@ * limitations under the License. */ -using System; using Newtonsoft.Json; +using RelationalAI.Errors; namespace RelationalAI.Utils { public class Json + where T : class { public static T Deserialize(string data, string key = null) { - if (string.IsNullOrEmpty(data) || data == "[]") + if (string.IsNullOrEmpty(data)) { - throw new SystemException("404 not found"); + return null; } try @@ -34,7 +35,7 @@ public static T Deserialize(string data, string key = null) } catch { - throw new SystemException(data); + throw new InvalidResponseException($"Failed to deserialize response into type {typeof(T).Name}", data); } } }