# HTTP Validation HTTP validation with RFC 7807 Problem Details for structured error responses. ## Overview When validation fails for HTTP endpoints, the framework returns structured error responses following **RFC 7807 Problem Details for HTTP APIs** standard. This provides: - ✅ **Standardized format** - Consistent error structure across all endpoints - ✅ **Machine-readable** - Clients can parse errors programmatically - ✅ **Human-friendly** - Clear messages for debugging - ✅ **Field-level errors** - Know exactly which fields failed validation - ✅ **HTTP 400 status** - Standard Bad Request response ## RFC 7807 Problem Details ### Standard Format ```json { "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1", "title": "One or more validation errors occurred.", "status": 400, "errors": { "Name": ["Name is required"], "Email": ["Valid email address is required"], "Age": ["User must be at least 18 years old"] } } ``` ### Response Headers ``` HTTP/1.1 400 Bad Request Content-Type: application/problem+json ``` ## ASP.NET Core Integration ### Enable Problem Details ```csharp var builder = WebApplication.CreateBuilder(args); // Enable Problem Details builder.Services.AddProblemDetails(); var app = builder.Build(); // Use Problem Details middleware app.UseExceptionHandler(); app.UseStatusCodePages(); app.Run(); ``` ### Automatic Validation Validation happens automatically when using Minimal API endpoints: ```csharp // Command with validator public record CreateUserCommand { public string Name { get; init; } = string.Empty; public string Email { get; init; } = string.Empty; } public class CreateUserCommandValidator : AbstractValidator { public CreateUserCommandValidator() { RuleFor(x => x.Name) .NotEmpty() .WithMessage("Name is required"); RuleFor(x => x.Email) .EmailAddress() .WithMessage("Valid email address is required"); } } // Registration builder.Services.AddCommand(); builder.Services.AddTransient, CreateUserCommandValidator>(); // Endpoint automatically validates app.MapSvrntyCommands(); ``` ### Validation Flow ``` POST /api/command/createUser Body: { "name": "", "email": "invalid" } │ ▼ ┌──────────────────┐ │ Model Binding │ └──────┬───────────┘ │ ▼ ┌──────────────────┐ │ Run Validator │ └──────┬───────────┘ │ ├─ Valid ──────────▶ Execute Handler ──▶ 200 OK │ └─ Invalid ────────▶ Return Problem Details ──▶ 400 Bad Request ``` ## Example Validation Errors ### Single Field Error ```bash curl -X POST http://localhost:5000/api/command/createUser \ -H "Content-Type: application/json" \ -d '{"name": "", "email": "john@example.com"}' ``` **Response:** ```json { "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1", "title": "One or more validation errors occurred.", "status": 400, "errors": { "Name": ["Name is required"] } } ``` ### Multiple Field Errors ```bash curl -X POST http://localhost:5000/api/command/createUser \ -H "Content-Type: application/json" \ -d '{"name": "", "email": "invalid", "age": 16}' ``` **Response:** ```json { "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1", "title": "One or more validation errors occurred.", "status": 400, "errors": { "Name": ["Name is required"], "Email": ["Valid email address is required"], "Age": ["User must be at least 18 years old"] } } ``` ### Multiple Errors Per Field ```bash curl -X POST http://localhost:5000/api/command/createUser \ -H "Content-Type: application/json" \ -d '{"name": "A very long name that exceeds the maximum allowed length of 100 characters and should trigger a validation error"}' ``` **Response:** ```json { "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1", "title": "One or more validation errors occurred.", "status": 400, "errors": { "Name": [ "Name must not exceed 100 characters" ] } } ``` ## Custom Error Messages ### Override Default Messages ```csharp public class CreateUserCommandValidator : AbstractValidator { public CreateUserCommandValidator() { RuleFor(x => x.Name) .NotEmpty() .WithMessage("Please provide a name") .MaximumLength(100) .WithMessage("Name is too long (max 100 characters)"); RuleFor(x => x.Email) .EmailAddress() .WithMessage("Please provide a valid email address"); } } ``` **Response:** ```json { "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1", "title": "One or more validation errors occurred.", "status": 400, "errors": { "Name": ["Please provide a name"], "Email": ["Please provide a valid email address"] } } ``` ### Message Placeholders ```csharp RuleFor(x => x.Name) .MaximumLength(100) .WithMessage("Name must not exceed {MaxLength} characters. You entered {TotalLength} characters."); ``` **Response:** ```json { "errors": { "Name": ["Name must not exceed 100 characters. You entered 125 characters."] } } ``` ## Client-Side Handling ### JavaScript/TypeScript ```typescript interface ProblemDetails { type: string; title: string; status: number; errors?: { [key: string]: string[] }; } async function createUser(data: CreateUserCommand) { const response = await fetch('/api/command/createUser', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); if (response.status === 400) { const problem: ProblemDetails = await response.json(); // Display field-level errors for (const [field, errors] of Object.entries(problem.errors || {})) { console.error(`${field}: ${errors.join(', ')}`); } return null; } if (response.ok) { return await response.json(); } throw new Error('Unexpected error'); } ``` ### C# HttpClient ```csharp public class UserApiClient { private readonly HttpClient _httpClient; public async Task CreateUserAsync(CreateUserCommand command) { var response = await _httpClient.PostAsJsonAsync("/api/command/createUser", command); if (response.StatusCode == HttpStatusCode.BadRequest) { var problem = await response.Content.ReadFromJsonAsync(); if (problem?.Errors != null) { foreach (var (field, errors) in problem.Errors) { Console.WriteLine($"{field}: {string.Join(", ", errors)}"); } } return null; } response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync(); } } ``` ## Testing Validation ### Integration Tests ```csharp public class CreateUserCommandTests : IClassFixture> { private readonly HttpClient _client; public CreateUserCommandTests(WebApplicationFactory factory) { _client = factory.CreateClient(); } [Fact] public async Task CreateUser_WithMissingName_Returns400() { var command = new { name = "", email = "john@example.com" }; var response = await _client.PostAsJsonAsync("/api/command/createUser", command); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); var problem = await response.Content.ReadFromJsonAsync(); Assert.NotNull(problem); Assert.True(problem.Errors.ContainsKey("Name")); } [Fact] public async Task CreateUser_WithInvalidEmail_Returns400() { var command = new { name = "John", email = "invalid" }; var response = await _client.PostAsJsonAsync("/api/command/createUser", command); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); var problem = await response.Content.ReadFromJsonAsync(); Assert.Contains("Email", problem.Errors.Keys); } [Fact] public async Task CreateUser_WithValidData_Returns200() { var command = new { name = "John Doe", email = "john@example.com" }; var response = await _client.PostAsJsonAsync("/api/command/createUser", command); response.EnsureSuccessStatusCode(); var userId = await response.Content.ReadFromJsonAsync(); Assert.True(userId > 0); } } ``` ## Swagger/OpenAPI Integration Validation errors are automatically documented in Swagger: ```csharp builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var app = builder.Build(); app.UseSwagger(); app.UseSwaggerUI(); ``` **Swagger shows:** - 200 OK with response schema - 400 Bad Request with ProblemDetails schema - Field descriptions and constraints ## Custom Problem Details ### Add Additional Information ```csharp public class CustomProblemDetailsFactory : ProblemDetailsFactory { public override ValidationProblemDetails CreateValidationProblemDetails( HttpContext httpContext, ModelStateDictionary modelStateDictionary, int? statusCode = null, string? title = null, string? type = null, string? detail = null, string? instance = null) { var problemDetails = base.CreateValidationProblemDetails( httpContext, modelStateDictionary, statusCode, title, type, detail, instance); // Add custom fields problemDetails.Extensions["traceId"] = httpContext.TraceIdentifier; problemDetails.Extensions["timestamp"] = DateTime.UtcNow; return problemDetails; } } // Register builder.Services.AddSingleton(); ``` ## Best Practices ### ✅ DO - Use RFC 7807 Problem Details format - Return HTTP 400 for validation errors - Provide field-level error messages - Use descriptive error messages - Test validation scenarios - Document validation rules in OpenAPI - Handle validation errors gracefully in clients ### ❌ DON'T - Don't return HTTP 500 for validation errors - Don't use generic error messages - Don't expose internal details - Don't skip validation - Don't return HTML error pages for API endpoints - Don't throw exceptions for validation failures ## See Also - [Validation Overview](README.md) - [FluentValidation Setup](fluentvalidation-setup.md) - [gRPC Validation](grpc-validation.md) - [RFC 7807 Specification](https://tools.ietf.org/html/rfc7807) - [ASP.NET Core Problem Details](https://learn.microsoft.com/en-us/aspnet/core/web-api/handle-errors)