1

I'm trying to prepend the string )]}',\n to any response body that's JSON. I thought that an IAsyncResultFilter would be what I needed to use, but I'm not having luck. If I use the below code, it appends the text to the response since calling await next() writes to the response pipe. If I try and look at the context before that though, I can't tell what the response will actually be to know if it's JSON.

public class JsonPrefixFilter : IAsyncResultFilter
{
    public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
    {
        var executed = await next();
        var response = executed.HttpContext.Response;
        if (response.ContentType == null || !response.ContentType.StartsWith("application/json"))
            return;

        var prefix = Encoding.UTF8.GetBytes(")]}',\\n");
        var bytes = new ReadOnlyMemory<byte>(prefix);
        await response.BodyWriter.WriteAsync(bytes);
    }
}

2 Answers 2

1

Thanks to timur's post I was able to come up with this working solution.

public class JsonPrefixFilter : IAsyncResultFilter
{
    public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
    {
        var response = context.HttpContext.Response;

        // ASP.NET Core will always send the contents of the original Body stream back to the client.
        var originalBody = response.Body;

        // We want to write into a memory stream instead of the actual response body for now.
        var ms = new MemoryStream();
        response.Body = ms;

        // After this call the body is written into the memory stream and the properties
        // of the response object are populated.
        await next();

        if (response.ContentType != null && response.ContentType.StartsWith("application/json")) {
            var prefix = Encoding.UTF8.GetBytes(")]}',\\n");

            var prefixMemoryStream = new MemoryStream();
            await prefixMemoryStream.WriteAsync(prefix);
            await prefixMemoryStream.WriteAsync(ms.ToArray());
            prefixMemoryStream.Seek(0, SeekOrigin.Begin);

            // Now put the stream back that .NET wants to use and copy the memory stream to it.
            response.Body = originalBody;
            await prefixMemoryStream.CopyToAsync(response.Body);
        } else {
            // If it's not JSON, don't muck with the stream, so just put things back.
            response.Body = originalBody;
            ms.Seek(0, SeekOrigin.Begin);
            await ms.CopyToAsync(response.Body);
        }
    }
}

Update:

I never liked the above, so I switched to this solution. Instead of calling AddJsonOptions, I took inspiration from ASP.NET's formatter to use this instead:

public class XssJsonOutputFormatter : TextOutputFormatter
{
    private static readonly byte[] XssPrefix = Encoding.UTF8.GetBytes(")]}',\n");

    public JsonSerializerOptions SerializerOptions { get; }

    public XssJsonOutputFormatter()
    {
        SerializerOptions = new() {
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
            ReferenceHandler = ReferenceHandler.IgnoreCycles
        };

        SupportedEncodings.Add(Encoding.UTF8);
        SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json"));
    }

    public override sealed async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
    {
        ArgumentNullException.ThrowIfNull(context, nameof(context));
        ArgumentNullException.ThrowIfNull(selectedEncoding, nameof(selectedEncoding));

        var httpContext = context.HttpContext;
        var objectType = context.Object?.GetType() ?? context.ObjectType ?? typeof(object);

        var responseStream = httpContext.Response.Body;
        try {
            await responseStream.WriteAsync(XssPrefix);
            await JsonSerializer.SerializeAsync(responseStream, context.Object, objectType, SerializerOptions, httpContext.RequestAborted);
            await responseStream.FlushAsync(httpContext.RequestAborted);
        } catch (OperationCanceledException) when (context.HttpContext.RequestAborted.IsCancellationRequested) {
        }
    }
}

Now, when you call .AddControllers() you just set that as the first output formatter:

services.AddControllers(options => {
    options.Filters.Add(new ProducesAttribute("application/json"));
    options.OutputFormatters.Insert(0, new XssJsonOutputFormatter());
});

Obviously you could improve this to take serialization options in the constructor, but all my project would work exactly like the above so I just hardcoded it right in.

Sign up to request clarification or add additional context in comments.

4 Comments

Very Helpful -- Thanks for sharing. One improvement though is that the internal memory stream var ms = new MemoryStream(); should be explicitly disposed of after it's written back to the Original stream/Body stream.
@CajunCoding Please see updated way of doing this.
thanks for providing the update, definitely cleaner! I’m sure it’s useful for many as this post was easy to find….I may look at refactoring a custom ‘CompressResponseAttribute’ similarly if the route metadata is available in the output formatter (hmm)…because it needs to only apply when the Attribute exists…
Quick Followup/Update . . . the top/first approach (manipulating the Response stream) actually failed for us in AspNetCore 6 running in IIS (or IISExpress)... So I had to take yet another approach for manipulating the response, and Middleware is one of the more elegant ways to do it. And any middleware can be added at the route level with very little code using MiddlewareFilterAttribute... my use case of route specific compression example is here stackoverflow.com/a/73382952/7293142
0
+50

You could've used Seek on a steam to rewind it. Issue is, you can only keep adding onto default HttpResponseStream, it does not support seeking. So you can employ the technique from this SO answer and temporarily replace it with MemoryStream:

private Stream ReplaceBody(HttpResponse response)
{
    var originBody = response.Body;
    response.Body = new MemoryStream();
    return originBody;
}
private async Task ReturnBodyAsync(HttpResponse response, Stream originalBody)
{
    response.Body.Seek(0, SeekOrigin.Begin);
    await response.Body.CopyToAsync(originalBody);
    response.Body = originalBody;
}
public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
{
    var originalBody = ReplaceBody(context.HttpContext.Response); // replace the default stream with MemoryStream

    await next(); // we probably dont care about the return of this call. it's all in the context
    var response = context.HttpContext.Response;
    if (response.ContentType == null || !response.ContentType.StartsWith("application/json"))
        return;

    var prefix = Encoding.UTF8.GetBytes(")]}',\\n");
    var bytes = new ReadOnlyMemory<byte>(prefix);
    response.Body.Seek(0, SeekOrigin.Begin); // now you can seek. but you will notice that it overwrites the response so you might need to make extra space in the buffer
    await response.BodyWriter.WriteAsync(bytes);

    await ReturnBodyAsync(context.HttpContext.Response, originalBody); // revert the reference, copy data into default stream and return it
}

this is further complicated by the fact that you need to restore reference to original stream, so you have to careful around that.

This SO answer has a bit more context.

1 Comment

Thanks! This didn't actually work as-is, but based on this answer and the other SO answer that you pointed me to I was able to make it work. Once SO lets me award the bounty I'll pass it your way.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.