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

12 KiB

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:

var app = builder.Build();

// Missing MapSvrntyCommands() or MapSvrntyQueries()
app.Run();

Solution:

var app = builder.Build();

app.MapSvrntyCommands();   // Add this
app.MapSvrntyQueries();    // Add this

app.Run();

2. Wrong URL

Problem:

# 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:

// Missing registration
// builder.Services.AddCommand<CreateUserCommand, int, CreateUserCommandHandler>();

Solution:

builder.Services.AddCommand<CreateUserCommand, int, CreateUserCommandHandler>();

4. [IgnoreCommand] or [IgnoreQuery]

Problem:

[IgnoreCommand]  // This prevents endpoint generation
public record CreateUserCommand { }

Solution: Remove the attribute if you want an HTTP endpoint.

Debugging

// 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:

{
  "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:

{
  "email": "john@example.com"
  // Missing "name" field
}

Solution: Include all required fields.

2. Invalid Data Format

Problem:

{
  "age": "twenty-five"  // String instead of number
}

Solution:

{
  "age": 25
}

3. Validation Rules Violated

Validator:

RuleFor(x => x.Age)
    .GreaterThanOrEqualTo(18);

Request:

{
  "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:

curl -X POST http://localhost:5000/api/command/createUser
# No Authorization header

Solution:

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

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:

public Task<bool> CanExecuteAsync(...)
{
    return Task.FromResult(false);  // Always denies
}

Solution: Check authorization logic.

2. Missing Role

Problem:

app.MapSvrntyCommands().RequireAuthorization(policy =>
{
    policy.RequireRole("Admin");  // User doesn't have Admin role
});

Solution: Ensure user has required role.

Debugging

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:

curl -X POST http://localhost:5000/api/command/createUser \
  -d '{"name":"John"}'
# Missing -H "Content-Type: application/json"

Solution:

curl -X POST http://localhost:5000/api/command/createUser \
  -H "Content-Type: application/json" \
  -d '{"name":"John"}'

2. Wrong Content-Type

Problem:

-H "Content-Type: text/plain"

Solution:

-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:

public async Task<UserDto> HandleAsync(GetUserQuery query, CancellationToken cancellationToken)
{
    var user = await _repository.GetByIdAsync(query.UserId, cancellationToken);
    return MapToDto(user);  // user is null!
}

Solution:

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:

builder.Services.AddScoped<IUserRepository, UserRepository>();

Debugging

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:

// Missing CORS configuration
var app = builder.Build();
app.MapSvrntyCommands();

Solution:

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:

app.MapSvrntyCommands();
app.UseCors();  // Too late!

Solution:

app.UseCors();  // Must be before MapSvrntyCommands
app.MapSvrntyCommands();

3. Credentials Without Specific Origin

Problem:

policy.AllowAnyOrigin()
    .AllowCredentials();  // Error: Can't use both

Solution:

policy.WithOrigins("https://example.com")
    .AllowCredentials();

JSON Serialization Issues

Issue: Properties not deserializing

Problem:

public record CreateUserCommand
{
    public string Name { get; init; } = string.Empty;
}

Request:

{
  "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:

builder.Services.ConfigureHttpJsonOptions(options =>
{
    options.SerializerOptions.PropertyNameCaseInsensitive = true;  // Default
});

Issue: Circular reference

Problem:

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:

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:

// Missing handler registration

Solution:

builder.Services.AddCommand<CreateUserCommand, int, CreateUserCommandHandler>();

2. Wrong Lifetime

Problem:

// Handler depends on Scoped service but registered as Singleton
builder.Services.AddSingleton<CreateUserCommandHandler>();

Solution:

builder.Services.AddCommand<CreateUserCommand, int, CreateUserCommandHandler>();
// Uses Scoped lifetime by default

Debugging Tools

Enable Detailed Errors

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}

Logging

builder.Logging.AddConsole();
builder.Logging.SetMinimumLevel(LogLevel.Debug);

Request/Response Logging

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

# Wrong - Queries support GET/POST, not PUT
curl -X PUT http://localhost:5000/api/query/getUser

Missing await

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

// 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