diff --git a/src/Mindee.Cli/Commands/V2/SearchModelsCommand.cs b/src/Mindee.Cli/Commands/V2/SearchModelsCommand.cs index 63a7bac1..72e7720d 100644 --- a/src/Mindee.Cli/Commands/V2/SearchModelsCommand.cs +++ b/src/Mindee.Cli/Commands/V2/SearchModelsCommand.cs @@ -129,7 +129,7 @@ public class Handler(V2Client mindeeClientV2) /// public async Task InvokeAsync(string? name, string? modelType, bool raw) { - var response = await mindeeClientV2.SearchModels(name, modelType); + var response = await mindeeClientV2.SearchModelsAsync(name, modelType); PrintToConsole(Console.Out, raw, response); return 0; } diff --git a/src/Mindee/V1/Exceptions/MindeeHttpExceptionV1Typed.cs b/src/Mindee/V1/Exceptions/MindeeHttpExceptionV1Typed.cs new file mode 100644 index 00000000..62a26c83 --- /dev/null +++ b/src/Mindee/V1/Exceptions/MindeeHttpExceptionV1Typed.cs @@ -0,0 +1,69 @@ +using System.Text.Json.Nodes; +using Mindee.V1.Parsing.Common; + +namespace Mindee.V1.Exceptions +{ + /// Bad Request (400). + public sealed class MindeeHttp400ExceptionV1 : MindeeHttpExceptionV1 + { + /// + public MindeeHttp400ExceptionV1(string name, string message, ErrorDetails details) + : base(name, message, details, 400) { } + } + + /// Unauthorized — invalid or missing API key (401). + public sealed class MindeeHttp401ExceptionV1 : MindeeHttpExceptionV1 + { + /// + public MindeeHttp401ExceptionV1(string name, string message, ErrorDetails details) + : base(name, message, details, 401) { } + } + + /// Forbidden — API key lacks permission (403). + public sealed class MindeeHttp403ExceptionV1 : MindeeHttpExceptionV1 + { + /// + public MindeeHttp403ExceptionV1(string name, string message, ErrorDetails details) + : base(name, message, details, 403) { } + } + + /// Not Found (404). + public sealed class MindeeHttp404ExceptionV1 : MindeeHttpExceptionV1 + { + /// + public MindeeHttp404ExceptionV1(string name, string message, ErrorDetails details) + : base(name, message, details, 404) { } + } + + /// Document too large (413). + public sealed class MindeeHttp413ExceptionV1 : MindeeHttpExceptionV1 + { + /// + public MindeeHttp413ExceptionV1(string name, string message, ErrorDetails details) + : base(name, message, details, 413) { } + } + + /// Too Many Requests — rate limit exceeded (429). + public sealed class MindeeHttp429ExceptionV1 : MindeeHttpExceptionV1 + { + /// + public MindeeHttp429ExceptionV1(string name, string message, ErrorDetails details) + : base(name, message, details, 429) { } + } + + /// Internal Server Error (500). + public sealed class MindeeHttp500ExceptionV1 : MindeeHttpExceptionV1 + { + /// + public MindeeHttp500ExceptionV1(string name, string message, ErrorDetails details) + : base(name, message, details, 500) { } + } + + /// Gateway Timeout / Request Timeout (504). + public sealed class MindeeHttp504ExceptionV1 : MindeeHttpExceptionV1 + { + /// + public MindeeHttp504ExceptionV1(string name, string message, ErrorDetails details) + : base(name, message, details, 504) { } + } +} diff --git a/src/Mindee/V1/Http/MindeeApi.cs b/src/Mindee/V1/Http/MindeeApi.cs index c5c3a87e..dfb25dfa 100644 --- a/src/Mindee/V1/Http/MindeeApi.cs +++ b/src/Mindee/V1/Http/MindeeApi.cs @@ -262,33 +262,73 @@ private TModel ResponseHandler(RestResponse restResponse) } } - // Always add the status code, this REALLY should be made a property of our HTTP error classes. - errorMessage += $"HTTP code {restResponse.StatusCode}: "; + var statusCode = (int)restResponse.StatusCode; + errorMessage += $"HTTP code {statusCode}: "; - // If the server JSON return is empty, try to dump the raw contents. - // If that STILL doesn't work, notify the user that the server response is empty. var apiErrorMessage = model?.ApiRequest.Error.ToString(); + string errorName; + ErrorDetails errorDetails; + if (!string.IsNullOrEmpty(apiErrorMessage)) { + // JSON error body parsed successfully errorMessage += apiErrorMessage; - } - else if (!string.IsNullOrEmpty(restResponse.Content)) - { - errorMessage += restResponse.Content; + errorName = model.ApiRequest.Error.Code ?? "MindeeHttpError"; + errorDetails = model.ApiRequest.Error.Details; } else { - errorMessage += "Empty response from server."; + // Non-JSON (likely HTML) — apply substring heuristics + var body = restResponse.Content ?? string.Empty; + errorName = ClassifyHtmlError(body); + errorMessage += string.IsNullOrEmpty(body) ? "Empty response from server." : body; + errorDetails = null; } _logger?.LogError("{ErrorMessage}", errorMessage); - throw new MindeeHttpExceptionV1( - "MindeeHttpError", - errorMessage, - model?.ApiRequest.Error.Details, - (int)restResponse.StatusCode - ); + throw BuildHttpException(errorName, errorMessage, errorDetails, statusCode); + } + + /// + /// Classifies a non-JSON (HTML) error body into a meaningful error-name string, + /// matching the heuristics used by sibling SDKs (Python / Node). + /// + private static string ClassifyHtmlError(string body) + { + if (body.Contains("Maximum pdf pages", StringComparison.OrdinalIgnoreCase)) + return "TooManyPages"; + if (body.Contains("Max file size is", StringComparison.OrdinalIgnoreCase)) + return "FileTooLarge"; + if (body.Contains("Invalid file type", StringComparison.OrdinalIgnoreCase)) + return "InvalidFiletype"; + if (body.Contains("Gateway timeout", StringComparison.OrdinalIgnoreCase)) + return "RequestTimeout"; + if (body.Contains("Bad gateway", StringComparison.OrdinalIgnoreCase)) + return "BadRequest"; + if (body.Contains("Too Many Requests", StringComparison.OrdinalIgnoreCase)) + return "TooManyRequests"; + return "UnknownError"; + } + + /// + /// Constructs the most specific typed exception for the given HTTP status code. + /// + private static MindeeHttpExceptionV1 BuildHttpException( + string name, string message, ErrorDetails details, int statusCode) + { + return statusCode switch + { + 400 => new MindeeHttp400ExceptionV1(name, message, details), + 401 => new MindeeHttp401ExceptionV1(name, message, details), + 403 => new MindeeHttp403ExceptionV1(name, message, details), + 404 => new MindeeHttp404ExceptionV1(name, message, details), + 413 => new MindeeHttp413ExceptionV1(name, message, details), + 429 => new MindeeHttp429ExceptionV1(name, message, details), + 500 => new MindeeHttp500ExceptionV1(name, message, details), + 504 => new MindeeHttp504ExceptionV1(name, message, details), + _ => new MindeeHttpExceptionV1(name, message, details, statusCode), + }; } } } diff --git a/src/Mindee/V2/Client.cs b/src/Mindee/V2/Client.cs index e96d4d3f..eaa27de4 100644 --- a/src/Mindee/V2/Client.cs +++ b/src/Mindee/V2/Client.cs @@ -264,9 +264,24 @@ InputSource inputSource /// Model type filter. /// Cancellation token. /// - public async Task SearchModels(string name = null, string modelType = null, CancellationToken ct = default) + public async Task SearchModelsAsync(string name = null, string modelType = null, CancellationToken ct = default) { - return await _mindeeApi.SearchModels(name, modelType, ct); + return await _mindeeApi.SearchModelsAsync(name, modelType, ct); + } + + /// + /// Searches for models. + /// (Deprecated: Please use instead.) + /// + /// + /// + /// + /// + [Obsolete("Please use SearchModelsAsync instead.")] + public async Task SearchModels(string name = null, string modelType = null, + CancellationToken ct = default) + { + return await SearchModelsAsync(name, modelType, ct); } /// diff --git a/src/Mindee/V2/Http/HttpApiV2.cs b/src/Mindee/V2/Http/HttpApiV2.cs index b1382f7d..cb9e3d45 100644 --- a/src/Mindee/V2/Http/HttpApiV2.cs +++ b/src/Mindee/V2/Http/HttpApiV2.cs @@ -75,7 +75,7 @@ public abstract class HttpApiV2 /// Type of the model to search for. /// Cancellation token. /// - public abstract Task SearchModels(string? name, string? modelType, CancellationToken ct = default); + public abstract Task SearchModelsAsync(string? name, string? modelType, CancellationToken ct = default); /// /// Get the error from the server return. diff --git a/src/Mindee/V2/Http/MindeeApiV2.cs b/src/Mindee/V2/Http/MindeeApiV2.cs index 021fb2f1..af35f29d 100644 --- a/src/Mindee/V2/Http/MindeeApiV2.cs +++ b/src/Mindee/V2/Http/MindeeApiV2.cs @@ -70,7 +70,7 @@ public override async Task ReqPostEnqueueAsync( return HandleJobResponse(response); } - public override async Task SearchModels(string name, string modelType, CancellationToken ct = default) + public override async Task SearchModelsAsync(string name, string modelType, CancellationToken ct = default) { var request = new RestRequest("v2/search/models"); Logger?.LogInformation("Fetching models..."); diff --git a/tests/Mindee.IntegrationTests/V1/ClientTest.cs b/tests/Mindee.IntegrationTests/V1/ClientTest.cs index 7ca6d630..0759c5e5 100644 --- a/tests/Mindee.IntegrationTests/V1/ClientTest.cs +++ b/tests/Mindee.IntegrationTests/V1/ClientTest.cs @@ -76,7 +76,7 @@ public async Task Parse_Url_Standard_SinglePage_MustSucceed() public async Task Parse_Url_Standard_InvalidUrl_MustFail() { var inputSource = new UrlInputSource("https://bad-domain.test/invalid-file.ext"); - await Assert.ThrowsAsync(() => _client.ParseAsync(inputSource)); + await Assert.ThrowsAnyAsync(() => _client.ParseAsync(inputSource)); } [Fact(Timeout = 180000)] @@ -154,7 +154,7 @@ public async Task Parse_File_Standard_AllWords_And_Cropper_MustSucceed() public async Task Enqueue_File_Standard_SyncOnly_Async_MustFail() { var inputSource = new LocalInputSource(Constants.V1ProductDir + "passport/default_sample.jpg"); - await Assert.ThrowsAsync(() => _client.EnqueueAsync(inputSource)); + await Assert.ThrowsAnyAsync(() => _client.EnqueueAsync(inputSource)); } [Fact(Timeout = 180000)] @@ -180,7 +180,7 @@ public async Task Enqueue_File_Standard_AsyncOnly_Async_MustSucceed() public async Task Enqueue_File_Standard_AsyncOnly_Sync_MustFail() { var inputSource = new LocalInputSource(Constants.V1ProductDir + "invoice_splitter/default_sample.pdf"); - await Assert.ThrowsAsync(() => + await Assert.ThrowsAnyAsync(() => _client.ParseAsync(inputSource)); } @@ -264,7 +264,7 @@ public async Task EnqueueAndParse_File_Standard_AsyncOnly_Async_UrlSource_Custom public async Task ParseQueued_Standard_InvalidJob_MustFail() { var jobId = RandomString(15); - await Assert.ThrowsAsync(() => + await Assert.ThrowsAnyAsync(() => _client.ParseQueuedAsync(jobId)); } @@ -273,7 +273,7 @@ public async Task Enqueue_File_Generated_AsyncOnly_Sync_MustFail() { var endpoint = new CustomEndpoint("international_id", "mindee", "2"); var inputSource = new LocalInputSource(Constants.V1ProductDir + "international_id/default_sample.jpg"); - await Assert.ThrowsAsync(() => + await Assert.ThrowsAnyAsync(() => _client.ParseAsync(inputSource, endpoint)); } @@ -308,7 +308,7 @@ public async Task ParseQueued_Generated_InvalidJob_MustFail() { var jobId = RandomString(15); var endpoint = new CustomEndpoint("international_id", "mindee", "2"); - await Assert.ThrowsAsync(() => + await Assert.ThrowsAnyAsync(() => _client.ParseQueuedAsync(endpoint, jobId)); } diff --git a/tests/Mindee.UnitTests/V1/Http/MindeeApiTest.cs b/tests/Mindee.UnitTests/V1/Http/MindeeApiTest.cs index 0dc49907..6e32fa48 100644 --- a/tests/Mindee.UnitTests/V1/Http/MindeeApiTest.cs +++ b/tests/Mindee.UnitTests/V1/Http/MindeeApiTest.cs @@ -24,10 +24,11 @@ public async Task Predict_WithWrongKey() var mindeeApi = serviceProvider.GetRequiredService(); - await Assert.ThrowsAsync(() => _ = mindeeApi.PredictPostAsync( + var ex = await Assert.ThrowsAnyAsync(() => _ = mindeeApi.PredictPostAsync( UnitTestBase.GetFakePredictParameter() ) ); + Assert.IsType(ex); } [Fact] @@ -61,9 +62,10 @@ public async Task Predict_WithErrorResponse() ); var mindeeApi = serviceProvider.GetRequiredService(); - await Assert.ThrowsAsync(() => mindeeApi.PredictPostAsync( + var ex = await Assert.ThrowsAnyAsync(() => mindeeApi.PredictPostAsync( UnitTestBase.GetFakePredictParameter()) ); + Assert.IsType(ex); } [Fact] @@ -74,9 +76,10 @@ public async Task Predict_500Error() File.ReadAllText(Constants.V1RootDir + "errors/error_500_inference_fail.json") ); var mindeeApi = serviceProvider.GetRequiredService(); - await Assert.ThrowsAsync(() => mindeeApi.PredictPostAsync( + var ex = await Assert.ThrowsAnyAsync(() => mindeeApi.PredictPostAsync( UnitTestBase.GetFakePredictParameter()) ); + Assert.IsType(ex); } [Fact] @@ -88,9 +91,10 @@ public async Task Predict_429Error() ); var mindeeApi = serviceProvider.GetRequiredService(); - await Assert.ThrowsAsync(() => mindeeApi.PredictPostAsync( + var ex = await Assert.ThrowsAnyAsync(() => mindeeApi.PredictPostAsync( UnitTestBase.GetFakePredictParameter()) ); + Assert.IsType(ex); } [Fact] @@ -102,9 +106,10 @@ public async Task Predict_401Error() ); var mindeeApi = serviceProvider.GetRequiredService(); - await Assert.ThrowsAsync(() => mindeeApi.PredictPostAsync( + var ex = await Assert.ThrowsAnyAsync(() => mindeeApi.PredictPostAsync( UnitTestBase.GetFakePredictParameter()) ); + Assert.IsType(ex); } [Fact] @@ -116,9 +121,10 @@ public async Task PredictAsyncPostAsync_WithFailForbidden() ); var mindeeApi = serviceProvider.GetRequiredService(); - await Assert.ThrowsAsync(() => mindeeApi.PredictAsyncPostAsync( + var ex = await Assert.ThrowsAnyAsync(() => mindeeApi.PredictAsyncPostAsync( UnitTestBase.GetFakePredictParameter()) ); + Assert.IsType(ex); } [Fact] @@ -169,7 +175,7 @@ public async Task DocumentQueueGetAsync_WithJobFailed() ); var mindeeApi = serviceProvider.GetRequiredService(); - await Assert.ThrowsAsync(() => + var ex = await Assert.ThrowsAnyAsync(() => mindeeApi.DocumentQueueGetAsync("d88406ed-47bd-4db0-b3f3-145c8667a343") ); }