RobIII
RobIII

Reputation: 8821

Output XML validation error from Web API Middleware (400 Bad request)

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?

Upvotes: -1

Views: 84

Answers (1)

Chen
Chen

Reputation: 5164

What does no body output mean? Is the response body empty? May be because you are writing to a NullStream (not to be confused with null value).

Default value of Body property of HttpResponse is precisely the NullStream. In a real scenario when an HTTP request arrives, the NullStream is replaced with HttpResponseStream. You won't be able to use it on your own as its accessibility level is set to internal.

You can replace the NullStream with any type of stream you want, for example the MemoryStream:

context.Response.Body = new MemoryStream();

Hope this can help you.

Upvotes: 0

Related Questions