Hallo!
Ich brauche Hilfe von den Experten. Folgendes Problem: Ich baue eine API mit ASP.NET Core 8, die mit JSON-Strings gefüttert wird. Diese API empfängt Daten von einem Möbelkonfigurator, der noch fertig entwickelt werden muss. Dadurch gestaltet sich alles seitens der Firma, die den Konfigurator entwickelt, etwas zu dynamisch.
An sich ist die API fertig und betriebsbereit. Allerdings möchte die andere Softwarefirma mehr Infos, wenn sie die API testen und dabei auf Probleme stoßen. Warum sie das nicht einfach mit Postman machen, weiß ich nicht.
Auf jeden Fall lasse ich ein Log mit Serilog laufen. Die Logausgabe erfolgt in einer Datei mit einem Zeitstempel im Dateinamen. Jeden Tag wird eine neue Logdatei begonnen und es werden maximal 90 vorgehalten. Diese Logs protokollieren ziemlich alles und zwar sehr ausführlich. Auch Fehler werden darin protokolliert. Deswegen habe ich der anderen Softwarefirma einen lesenden WebDAV-Zugang zu diesen Protokollen ermöglicht, damit sie sich darin austoben können. Bis hierhin alles gut.
Nun gibt es aber einige Fehler, die bereits vor der Deserialisierung auftreten. Zum Beispiel schicken sie Daten, in denen u. a. eine GUID enthalten ist, die weder leer noch null
sein darf, und sie finden nichts besseres, als null
zu schicken. An dieser Stelle tritt ein Fehler auf, den ich aber bisher nicht abfangen konnte. Der Fehler wird kurz wie folgt protokolliert:
2025-01-29 07:20:58.650 +01:00 [DBG] JSON input formatter threw an exception: The JSON value could not be converted to System.Guid. Path: $.retailerId | LineNumber: 17 | BytePositionInLine: 22.
Es ist mir klar, was da passiert: retailerId kann nicht in eine GUID konvertiert werden, und man muss eben danach schauen, was da passiert ist.
Ich habe u. a. einen globalen Filter:
public class GlobalExceptionFilter : IExceptionFilter
{
private readonly ILogger<GlobalExceptionFilter> _logger;
public GlobalExceptionFilter(ILogger<GlobalExceptionFilter> logger)
{
_logger = logger;
}
public void OnException(ExceptionContext context)
{
_logger.LogError(context.Exception, $"Eine Ausnahme ist aufgetreten: {context.Exception.Message}");
// ...
// Setze das Ergebnis auf einen generischen Fehler
context.Result = new ObjectResult(new { error = "Ein Fehler ist aufgetreten. Bitte überprüfen Sie die Protokolle für weitere Details." })
{
StatusCode = 500
};
context.ExceptionHandled = true;
}
}
Zusätzlich einen benutzerdefinierten Action-Filter, der verwendet wird, um Validierungsfehler im ModelState
zu überprüfen und zu protokollieren:
public class CustomModelStateInvalidFilter : IActionFilter
{
public int Order => -2100;
private readonly ILogger<CustomModelStateInvalidFilter> _logger;
public CustomModelStateInvalidFilter(ILogger<CustomModelStateInvalidFilter> logger)
{
_logger = logger;
}
public void OnActionExecuting(ActionExecutingContext context)
{
_logger.LogInformation("CustomModelStateInvalidFilter wurde ausgeführt.");
if (!context.ModelState.IsValid)
{
// ...
context.Result = new BadRequestObjectResult(problemDetails);
}
}
public void OnActionExecuted(ActionExecutedContext context)
{
// Keine Aktion erforderlich nach der Ausführung
}
}
Diese Filter werden im Program.cs
hinzugefügt:
builder.Services.AddControllers(options =>
{
options.Filters.Add<CustomModelStateInvalidFilter>();
options.Filters.Add<GlobalExceptionFilter>();
options.Filters.Add<ModelStateLoggerFilter>();
})
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.Preserve;
options.JsonSerializerOptions.WriteIndented = true;
});
Dann schließlich eine benutzerdefinierte Middleware, die Fehler bei der JSON-Deserialisierung sowie allgemeine Fehler abfangen und eine Fehlerantwort zurückgeben sollte:
namespace okpm.Middleware;
public class JsonExceptionHandlingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<JsonExceptionHandlingMiddleware> _logger;
public JsonExceptionHandlingMiddleware(RequestDelegate next, ILogger<JsonExceptionHandlingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task Invoke(HttpContext context)
{
try
{
await _next(context);
}
catch (JsonException ex)
{
_logger.LogError(ex, $"Fehler beim JSON-Parsing: {ex.Message}");
await HandleErrorAsync(context, ex, "Ungültige JSON-Daten.");
}
catch (InvalidOperationException ex) when (ex.Message.Contains("JSON input formatter"))
{
_logger.LogError(ex, $"Fehler beim JSON-Parsing: {ex.Message}");
await HandleErrorAsync(context, ex, "Fehler beim Verarbeiten der Anfrage.");
}
catch (Exception ex)
{
_logger.LogError(ex, $"Unbehandelter Fehler: {ex.Message}");
await HandleErrorAsync(context, ex, "Unbekannter Fehler.");
}
}
private Task HandleErrorAsync(HttpContext context, Exception ex, string message)
{
context.Response.StatusCode = StatusCodes.Status400BadRequest;
context.Response.ContentType = "application/json";
var response = new
{
message,
error = ex.Message
};
return context.Response.WriteAsync(JsonSerializer.Serialize(response));
}
}
Eingebunden wird sie im Program.cs:
app.UseMiddleware<Middleware.JsonExceptionHandlingMiddleware>();
Aber egal, was ich mache, ich kann so eine Ausnahme, die bei einem Problem beim Parsen von JSON-Daten auftritt (z. B. ungültiges JSON: retailerId = null
), nicht abfangen.
Schlimm ist es nicht, denn ASP.NET Core protokolliert mir den Fehler:
2025-01-29 07:20:58.650 +01:00 [DBG] JSON input formatter threw an exception: The JSON value could not be converted to System.Guid. Path: $.retailerId | LineNumber: 17 | BytePositionInLine: 22.
Allerdings dachte ich mir, ich kann hier eingreifen und selbst die Fehlermeldung etwas umfangreicher machen und sie aufhübschen. Bisher aber erfolglos.
Hat mir jemand einen Tipp?
Danke im Voraus und liebe Grüße
René
René
Also hiermit
public class SomeController : ControllerBase
{
[HttpPost( Name = "SetData" )]
public IActionResult Post( [FromBody] Data model )
{
return Ok();
}
}
public class Data
{
[Required]
public Guid? RetailerId { get; set; }
[Required]
public string? Name { get; set; }
}
public class ValidateModelAttribute : ActionFilterAttribute
{
public override void OnActionExecuting( ActionExecutingContext actionContext )
{
if ( actionContext.ModelState.IsValid == false )
{
actionContext.Result = new BadRequestObjectResult( actionContext.ModelState );
}
}
}
// Add services to the container.
builder.Services.AddControllers( options =>
{
options.Filters.Add<ValidateModelAttribute>();
} );
erhalte ich bei dem Aufruf
POST {{MyWebApi_HostAddress}}/some/
Content-Type: application/json
Accept: application/json
{
"retailerId": null,
"name": null
}
diesen response
HTTP/1.1 400 Bad Request
Connection: close
Content-Type: application/problem+json; charset=utf-8
Date: Wed, 29 Jan 2025 09:25:58 GMT
Server: Kestrel
Transfer-Encoding: chunked
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"Name": [
"The Name field is required."
],
"RetailerId": [
"The RetailerId field is required."
]
},
"traceId": "00-5d5349a341853bf4ad3939fc19697e5b-0fbb3215c4907859-00"
}
Ist das nicht das was du suchst?
Dabei stelle ich fest, dass der Filter dafür gar nicht gebraucht wird.
Hat die Blume einen Knick, war der Schmetterling zu dick.
Dabei stelle ich fest, dass der Filter dafür gar nicht gebraucht wird.
Lieben Dank! Ich denke, ich bin selbst durcheinander gekommen, nach einer langen Nacht.
Ich habe zwar die Fehler in der Response gesehen, nicht jedoch im Log. Nun habe ich mir das ganze genauer angeschaut und im Program.cs
nach dem Build()
folgenden Code eingefügt:
app.UseExceptionHandler(errorApp =>
{
errorApp.Run(async context =>
{
var logger = context.RequestServices.GetRequiredService<ILogger<Program>>();
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
context.Response.ContentType = "application/json";
// Exception-Details abrufen.
var exceptionHandlerFeature = context.Features.Get<IExceptionHandlerFeature>();
if (exceptionHandlerFeature?.Error is Exception exception)
{
// Fehler mit Stacktrace nur ins Log schreiben. "context.TraceIdentifier" hilft bei der Fehlerverfolgung im Log.
logger.LogError(exception, "Fehler {TraceId}: {ErrorMessage}", context.TraceIdentifier, exception.Message);
}
else
{
// Allgemeiner Fehler ohne Exception. "context.TraceIdentifier" hilft bei der Fehlerverfolgung im Log.
logger.LogError($"Unbekannter Fehler {context.TraceIdentifier}");
}
var errorDetails = new
{
error = "Ein interner Serverfehler ist aufgetreten.",
traceId = context.TraceIdentifier // Hilft bei der Fehlerverfolgung im Log.
};
await context.Response.WriteAsJsonAsync(errorDetails);
});
});
Nun scheint es jetzt zu funktionieren, auch wenn Müll frei Haus geliefert wird.
Die Middleware sorgt dafür, dass alle Fehler, die während einer HTTP-Anfrage passieren können, abgefangen werden. Dabei wird der Fehler ausführlich im Log gespeichert. An den Client wird nicht zu viel geschickt. Der bekommt nur eine allgemeine Fehlermeldung, allerdings mit der TraceId
– Diese hilft, den Fehler im Log später nachzuvollziehen. Im Log steht, was genau schiefgelaufen ist, auch mit dem Stacktrace.
Nochmals lieben Dank und herzliche Grüße!
René
René