dotnet-cqrs/docs/http-integration/http-troubleshooting.md

671 lines
12 KiB
Markdown

# HTTP Troubleshooting
Common HTTP integration issues and solutions.
## 404 Not Found
### Issue: Endpoint not found
**Symptoms:**
```
HTTP/1.1 404 Not Found
```
**Possible Causes:**
#### 1. Endpoint Not Mapped
**Problem:**
```csharp
var app = builder.Build();
// Missing MapSvrntyCommands() or MapSvrntyQueries()
app.Run();
```
**Solution:**
```csharp
var app = builder.Build();
app.MapSvrntyCommands(); // Add this
app.MapSvrntyQueries(); // Add this
app.Run();
```
#### 2. Wrong URL
**Problem:**
```bash
# Wrong
POST /api/command/CreateUser # Capital C
# Correct
POST /api/command/createUser # Lowercase c
```
URLs are case-sensitive in lowerCamelCase.
#### 3. Command/Query Not Registered
**Problem:**
```csharp
// Missing registration
// builder.Services.AddCommand<CreateUserCommand, int, CreateUserCommandHandler>();
```
**Solution:**
```csharp
builder.Services.AddCommand<CreateUserCommand, int, CreateUserCommandHandler>();
```
#### 4. [IgnoreCommand] or [IgnoreQuery]
**Problem:**
```csharp
[IgnoreCommand] // This prevents endpoint generation
public record CreateUserCommand { }
```
**Solution:**
Remove the attribute if you want an HTTP endpoint.
### Debugging
```csharp
// List all registered endpoints
var app = builder.Build();
app.MapSvrntyCommands();
app.MapSvrntyQueries();
// Before app.Run()
var endpoints = app.Services.GetRequiredService<EndpointDataSource>();
foreach (var endpoint in endpoints.Endpoints)
{
Console.WriteLine($"{endpoint.DisplayName}: {endpoint.Metadata}");
}
app.Run();
```
## 400 Bad Request
### Issue: Validation failed
**Symptoms:**
```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"]
}
}
```
**Causes:**
#### 1. Missing Required Fields
**Request:**
```json
{
"email": "john@example.com"
// Missing "name" field
}
```
**Solution:**
Include all required fields.
#### 2. Invalid Data Format
**Problem:**
```json
{
"age": "twenty-five" // String instead of number
}
```
**Solution:**
```json
{
"age": 25
}
```
#### 3. Validation Rules Violated
**Validator:**
```csharp
RuleFor(x => x.Age)
.GreaterThanOrEqualTo(18);
```
**Request:**
```json
{
"age": 16 // Fails validation
}
```
**Solution:**
Send valid data that meets all validation rules.
## 401 Unauthorized
### Issue: Missing or invalid authentication
**Symptoms:**
```
HTTP/1.1 401 Unauthorized
```
**Causes:**
#### 1. Missing Authentication Header
**Problem:**
```bash
curl -X POST http://localhost:5000/api/command/createUser
# No Authorization header
```
**Solution:**
```bash
curl -X POST http://localhost:5000/api/command/createUser \
-H "Authorization: Bearer eyJhbGc..."
```
#### 2. Expired Token
**Problem:**
JWT token has expired.
**Solution:**
Refresh the token or obtain a new one.
#### 3. Invalid Token
**Problem:**
Token is malformed or invalid.
**Solution:**
Verify token is correctly formatted and signed.
### Debugging
```csharp
app.Use(async (context, next) =>
{
var logger = context.RequestServices.GetRequiredService<ILogger<Program>>();
if (context.User.Identity?.IsAuthenticated == false)
{
logger.LogWarning("Unauthenticated request to {Path}", context.Request.Path);
}
await next();
});
```
## 403 Forbidden
### Issue: Not authorized
**Symptoms:**
```
HTTP/1.1 403 Forbidden
```
**Causes:**
#### 1. Authorization Service Denied
**Authorization Service:**
```csharp
public Task<bool> CanExecuteAsync(...)
{
return Task.FromResult(false); // Always denies
}
```
**Solution:**
Check authorization logic.
#### 2. Missing Role
**Problem:**
```csharp
app.MapSvrntyCommands().RequireAuthorization(policy =>
{
policy.RequireRole("Admin"); // User doesn't have Admin role
});
```
**Solution:**
Ensure user has required role.
### Debugging
```csharp
public class DeleteUserCommandAuthorization : ICommandAuthorizationService<DeleteUserCommand>
{
private readonly ILogger<DeleteUserCommandAuthorization> _logger;
public async Task<bool> CanExecuteAsync(
DeleteUserCommand command,
ClaimsPrincipal user,
CancellationToken cancellationToken)
{
var isAdmin = user.IsInRole("Admin");
_logger.LogInformation(
"Authorization check for DeleteUser: UserId={UserId}, IsAdmin={IsAdmin}",
command.UserId,
isAdmin);
if (!isAdmin)
{
_logger.LogWarning(
"User {UserName} attempted to delete user {UserId} without Admin role",
user.Identity?.Name,
command.UserId);
}
return isAdmin;
}
}
```
## 415 Unsupported Media Type
### Issue: Wrong Content-Type
**Symptoms:**
```
HTTP/1.1 415 Unsupported Media Type
```
**Causes:**
#### 1. Missing Content-Type Header
**Problem:**
```bash
curl -X POST http://localhost:5000/api/command/createUser \
-d '{"name":"John"}'
# Missing -H "Content-Type: application/json"
```
**Solution:**
```bash
curl -X POST http://localhost:5000/api/command/createUser \
-H "Content-Type: application/json" \
-d '{"name":"John"}'
```
#### 2. Wrong Content-Type
**Problem:**
```bash
-H "Content-Type: text/plain"
```
**Solution:**
```bash
-H "Content-Type: application/json"
```
## 500 Internal Server Error
### Issue: Unhandled exception
**Symptoms:**
```
HTTP/1.1 500 Internal Server Error
```
**Common Causes:**
#### 1. Null Reference Exception
**Problem:**
```csharp
public async Task<UserDto> HandleAsync(GetUserQuery query, CancellationToken cancellationToken)
{
var user = await _repository.GetByIdAsync(query.UserId, cancellationToken);
return MapToDto(user); // user is null!
}
```
**Solution:**
```csharp
public async Task<UserDto> HandleAsync(GetUserQuery query, CancellationToken cancellationToken)
{
var user = await _repository.GetByIdAsync(query.UserId, cancellationToken);
if (user == null)
throw new KeyNotFoundException($"User {query.UserId} not found");
return MapToDto(user);
}
```
#### 2. Database Connection Issues
**Problem:**
```
Connection to database failed
```
**Solution:**
- Check connection string
- Verify database is running
- Check firewall rules
- Verify credentials
#### 3. Missing Dependency
**Problem:**
```
Cannot resolve service for type 'IUserRepository'
```
**Solution:**
```csharp
builder.Services.AddScoped<IUserRepository, UserRepository>();
```
### Debugging
```csharp
app.UseExceptionHandler(errorApp =>
{
errorApp.Run(async context =>
{
var exceptionHandlerFeature = context.Features.Get<IExceptionHandlerFeature>();
var exception = exceptionHandlerFeature?.Error;
var logger = context.RequestServices.GetRequiredService<ILogger<Program>>();
logger.LogError(exception, "Unhandled exception occurred");
context.Response.StatusCode = 500;
context.Response.ContentType = "application/json";
await context.Response.WriteAsJsonAsync(new
{
error = app.Environment.IsDevelopment() ? exception?.Message : "An error occurred",
stackTrace = app.Environment.IsDevelopment() ? exception?.StackTrace : null
});
});
});
```
## CORS Errors
### Issue: CORS policy blocks request
**Browser Console:**
```
Access to fetch at 'http://localhost:5000/api/command/createUser' from origin
'http://localhost:3000' has been blocked by CORS policy
```
**Causes:**
#### 1. CORS Not Configured
**Problem:**
```csharp
// Missing CORS configuration
var app = builder.Build();
app.MapSvrntyCommands();
```
**Solution:**
```csharp
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
{
policy.WithOrigins("http://localhost:3000")
.AllowAnyMethod()
.AllowAnyHeader();
});
});
var app = builder.Build();
app.UseCors(); // Add before MapSvrntyCommands
app.MapSvrntyCommands();
```
#### 2. Wrong Order
**Problem:**
```csharp
app.MapSvrntyCommands();
app.UseCors(); // Too late!
```
**Solution:**
```csharp
app.UseCors(); // Must be before MapSvrntyCommands
app.MapSvrntyCommands();
```
#### 3. Credentials Without Specific Origin
**Problem:**
```csharp
policy.AllowAnyOrigin()
.AllowCredentials(); // Error: Can't use both
```
**Solution:**
```csharp
policy.WithOrigins("https://example.com")
.AllowCredentials();
```
## JSON Serialization Issues
### Issue: Properties not deserializing
**Problem:**
```csharp
public record CreateUserCommand
{
public string Name { get; init; } = string.Empty;
}
```
**Request:**
```json
{
"name": "John Doe" // Lowercase 'name'
}
```
**Result:** Name is empty string (default value)
**Solution:**
JSON property names are case-insensitive by default in ASP.NET Core, but ensure:
```csharp
builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.PropertyNameCaseInsensitive = true; // Default
});
```
### Issue: Circular reference
**Problem:**
```csharp
public class User
{
public int Id { get; set; }
public List<Order> Orders { get; set; }
}
public class Order
{
public int Id { get; set; }
public User User { get; set; } // Circular reference!
}
```
**Solution:**
Use DTOs without circular references:
```csharp
public record UserDto
{
public int Id { get; init; }
public List<OrderSummaryDto> Orders { get; init; } = new();
}
public record OrderSummaryDto
{
public int Id { get; init; }
public decimal TotalAmount { get; init; }
// No User property - breaks the cycle
}
```
## Handler Not Found
### Issue: Handler not resolved from DI
**Symptoms:**
```
Unable to resolve service for type 'ICommandHandler<CreateUserCommand, int>'
```
**Causes:**
#### 1. Handler Not Registered
**Problem:**
```csharp
// Missing handler registration
```
**Solution:**
```csharp
builder.Services.AddCommand<CreateUserCommand, int, CreateUserCommandHandler>();
```
#### 2. Wrong Lifetime
**Problem:**
```csharp
// Handler depends on Scoped service but registered as Singleton
builder.Services.AddSingleton<CreateUserCommandHandler>();
```
**Solution:**
```csharp
builder.Services.AddCommand<CreateUserCommand, int, CreateUserCommandHandler>();
// Uses Scoped lifetime by default
```
## Debugging Tools
### Enable Detailed Errors
```csharp
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
```
### Logging
```csharp
builder.Logging.AddConsole();
builder.Logging.SetMinimumLevel(LogLevel.Debug);
```
### Request/Response Logging
```csharp
app.Use(async (context, next) =>
{
var logger = context.RequestServices.GetRequiredService<ILogger<Program>>();
logger.LogInformation(
"Request: {Method} {Path}",
context.Request.Method,
context.Request.Path);
await next();
logger.LogInformation(
"Response: {StatusCode}",
context.Response.StatusCode);
});
```
## Common Mistakes
### ❌ Wrong HTTP Method
```bash
# Wrong - Queries support GET/POST, not PUT
curl -X PUT http://localhost:5000/api/query/getUser
```
### ❌ Missing await
```csharp
public async Task<UserDto> HandleAsync(GetUserQuery query, CancellationToken cancellationToken)
{
var user = _repository.GetByIdAsync(query.UserId, cancellationToken); // Missing await!
return MapToDto(user); // Error: user is Task, not User
}
```
### ❌ Blocking async calls
```csharp
// Don't do this
var result = handler.HandleAsync(query, cancellationToken).Result; // Blocks thread
// Do this
var result = await handler.HandleAsync(query, cancellationToken);
```
## Best Practices
### ✅ DO
- Enable detailed errors in development
- Use proper logging
- Handle exceptions gracefully
- Return appropriate status codes
- Test with Swagger UI
- Use integration tests
- Log authorization failures
### ❌ DON'T
- Don't expose detailed errors in production
- Don't ignore validation errors
- Don't skip authentication/authorization
- Don't block async calls with .Result or .Wait()
- Don't swallow exceptions
## See Also
- [HTTP Integration Overview](README.md)
- [Endpoint Mapping](endpoint-mapping.md)
- [HTTP Configuration](http-configuration.md)
- [Swagger Integration](swagger-integration.md)