I'd like to validate XML posted to a controller against a given XSD before modelbinding is done. For this I wrote the following middleware:
using Microsoft.Extensions.Options;
using System.Net;
using System.Xml.Linq;
using System.Xml.Schema;
namespace MyProject.Middleware;
public class XmlValidatingMiddlewareOptions
{
public bool Enabled { get; init; } = false;
public IDictionary<string, string> XmlSchemas { get; init; } = new Dictionary<string, string>();
}
public class XmlValidatingMiddleware
{
private readonly XmlValidatingMiddlewareOptions _options;
private readonly RequestDelegate _next;
private readonly XmlSchemaSet _xmlschemaset = new();
public XmlValidatingMiddleware(RequestDelegate next, IOptions<XmlValidatingMiddlewareOptions> options)
{
_next = next ?? throw new ArgumentNullException(nameof(next));
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
foreach (var xskv in _options.XmlSchemas)
_xmlschemaset.Add(xskv.Key, xskv.Value);
}
public async Task Invoke(HttpContext context)
{
if (_options.Enabled && IsXMLContentType(context.Request.ContentType) && IsPost(context.Request.Method))
{
context.Request.EnableBuffering();
// Validate XML against XSD schema
var validationerrors = await ValidateAsync(_xmlschemaset, context.Request.Body).ConfigureAwait(false);
if (validationerrors.Any())
{
// Validation failed, provide feedback
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
await context.Response.WriteAsync(
$"One or more validation errors occurred:\n\n{string.Join("\n", validationerrors.Select(r => $"{r.Severity}: {r.Message}"))}"
).ConfigureAwait(false);
return;
}
context.Request.Body.Position = 0;
}
await _next.Invoke(context);
}
private async Task<IEnumerable<ValidationEventArgs>> ValidateAsync(XmlSchemaSet xmlSchemaSet, Stream stream, CancellationToken cancellationToken = default)
{
var exceptions = new List<ValidationEventArgs>();
var doc = await XDocument.LoadAsync(stream, LoadOptions.None, cancellationToken).ConfigureAwait(false);
doc.Validate(xmlSchemaSet, (s, e) => exceptions.Add(e), true);
return exceptions;
}
private bool IsPost(string? method)
=> "POST".Equals(method, StringComparison.OrdinalIgnoreCase);
private bool IsXMLContentType(string? contentType)
=> contentType is not null
&& (
contentType.Equals("application/xml", StringComparison.OrdinalIgnoreCase)
||
contentType.Equals("text/xml", StringComparison.OrdinalIgnoreCase)
);
}
As you can see I output the HTTP statuscode 400 (Bad Request) on this line:
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
Right after that I also write some information about the validation errors. However, the body is not output. It is when I change the status to, say, 200 OK. I am not aware of an HTTP status 400 not having a body?
So my question: Why is there no body output when I send a 400 Bad Request, or, if a 400 Bad Request isn't supposed to have a body, what would be the correct Http status when invalid XML is passed and I want to provide feedback on the warnings/errors found?