diff --git a/README.md b/README.md index 3cf7162f..f48c39be 100644 --- a/README.md +++ b/README.md @@ -128,28 +128,37 @@ var allDevices = await pages.FlattenToListAsync(); To resume pagination later, store `pagination.NextPageCursor` and pass it to `NextPageAsync` on a new pager with the same request parameters. -### Errors +### Error Handling Seam API errors raise a typed exception carrying the Seam error code, HTTP -status code, and the `seam-request-id` to include in support requests: +status code, and the `seam-request-id` to include in support requests. + +#### Validation errors + +When the API rejects a request because a parameter is invalid, it throws a +`SeamHttpInvalidInputException`. Look up messages for a parameter you are +already rendering, for example a field in a form: ```csharp try { - await seam.Devices.GetAsync(new() { DeviceId = deviceId }); + await seam.Devices.ListAsync(new() { DeviceIds = ["not-a-uuid"] }); } catch (SeamHttpInvalidInputException exception) { - foreach (var message in exception.GetValidationErrorMessages("device_id")) + foreach (var message in exception.GetValidationErrorMessages("device_ids")) Console.WriteLine(message); } -catch (SeamHttpUnauthorizedException) -{ - // Invalid or expired credentials. -} -catch (SeamHttpApiException exception) +``` + +Or read every parameter that failed validation to summarize the request: + +```csharp +foreach (var validationError in exception.ValidationErrors) { - Console.WriteLine($"{exception.Code} ({exception.RequestId})"); + Console.WriteLine( + $"{validationError.ParameterName}: {string.Join(", ", validationError.ErrorMessages)}" + ); } ``` diff --git a/src/Seam/Exceptions/SeamHttpInvalidInputException.cs b/src/Seam/Exceptions/SeamHttpInvalidInputException.cs index db8cbf74..a00b3ee7 100644 --- a/src/Seam/Exceptions/SeamHttpInvalidInputException.cs +++ b/src/Seam/Exceptions/SeamHttpInvalidInputException.cs @@ -4,6 +4,14 @@ namespace Seam { + /// + /// A request parameter that failed validation and its error messages. + /// + public sealed record SeamValidationError( + string ParameterName, + IReadOnlyList ErrorMessages + ); + /// /// Raised when the Seam API rejects the request parameters. /// @@ -23,6 +31,34 @@ public SeamHttpInvalidInputException( _validationErrors = validationErrors; } + /// + /// Validation errors, one entry per failed request parameter. + /// + public IReadOnlyList ValidationErrors + { + get + { + if (_validationErrors is not { ValueKind: JsonValueKind.Object } validationErrors) + return Array.Empty(); + + var errors = new List(); + foreach (var parameter in validationErrors.EnumerateObject()) + { + if (parameter.Name != "_errors") + { + errors.Add( + new SeamValidationError( + parameter.Name, + GetValidationErrorMessages(parameter.Name) + ) + ); + } + } + + return errors; + } + } + /// /// The validation messages for a request parameter, or an empty list when that parameter /// has none. diff --git a/test/Seam.Test/HttpErrorTests.cs b/test/Seam.Test/HttpErrorTests.cs index 493cab4d..95d58d93 100644 --- a/test/Seam.Test/HttpErrorTests.cs +++ b/test/Seam.Test/HttpErrorTests.cs @@ -64,6 +64,7 @@ public class InvalidInputTests "type": "invalid_input", "message": "Invalid input", "validation_errors": { + "_errors": ["Request is invalid"], "device_ids": { "_errors": ["Expected array, received number"] } }, "request_id": "request1" @@ -104,6 +105,9 @@ public async Task ThrowsInvalidInputExceptionWithValidationMessages() new[] { "Expected array, received number" }, exception.GetValidationErrorMessages("device_ids") ); + var validationError = Assert.Single(exception.ValidationErrors); + Assert.Equal("device_ids", validationError.ParameterName); + Assert.Equal(new[] { "Expected array, received number" }, validationError.ErrorMessages); } [Fact]