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")
);
}