Skip to content
This repository has been archived by the owner on Dec 14, 2018. It is now read-only.

Cannot write to Response.Body in 1.1 #5555

Closed
nrandell opened this issue Nov 21, 2016 · 12 comments
Closed

Cannot write to Response.Body in 1.1 #5555

nrandell opened this issue Nov 21, 2016 · 12 comments

Comments

@nrandell
Copy link

I've just updated an app to version 1.1 and some code that was working started throwing exception saying

System.InvalidOperationException: OnStarting cannot be set, response has already started.
    at Microsoft.AspNetCore.Server.Kestrel.Internal.Http.Frame.OnStarting(Func`2 callback, Object state)
    at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
    at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.<InvokeNextResourceFilter>d__22.MoveNext()

It turns out that the issue was down to writing to Response.Body in my MVC actions. I've worked around this by downloading to a byte array and then returning File(array, contentType).

However I could not easily find any reason behind this - is this an error, desired functionality or just something wrong I'm doing?

@rynowak
Copy link
Member

rynowak commented Nov 21, 2016

@DamianEdwards
Copy link
Member

Thanks for reporting this. It appears we're missing a check there. We'll look in to getting this fixed in the next patch release.

@DamianEdwards DamianEdwards added this to the 1.1.1 milestone Nov 21, 2016
@kichalla
Copy link
Member

@nrandell Could you share more details about your app? Example the action code that you had earlier etc. The mentioned code in SaveTempDataFilter happens when executing an action result and if you were writing to the response stream directly, you might not have been returning an action result, so wanted to check. Also I did a quick verification and couldn't repro it.

@nrandell
Copy link
Author

This was my original code that worked before the upgrade. It basically downloads a blob from azure storage and streams that back to the client.

    [HttpGet]
    [Route("configuration/{id}")]
    [ResponseCache(Duration = 10)]
    public async Task ReadConfiguration(Guid id)
    {
        byte[] data;
        if (!_cache.TryGetValue(id, out data))
        {
            var container = _blobClient.GetContainerReference(id.ToString("D"));
            var blob = container.GetBlobReference("index.json");
            using (var ms = new MemoryStream())
            {
                await blob.DownloadToStreamAsync(ms);
                data = ms.ToArray();
                _cache.Set(id, data, TimeSpan.FromSeconds(10));
            }
        }
        Response.ContentType = "application/json";
        await Response.Body.WriteAsync(data, 0, data.Length);
    }

In trying to get it to work I made many changes, but ultimately ended up with

    [HttpGet]
    [Route("configuration/{id}")]
    [ResponseCache(Duration = 10)]
    public async Task<IActionResult> ReadConfiguration(Guid id)
    {
        byte[] data;
        if (!_cache.TryGetValue(id, out data))
        {
            var container = _blobClient.GetContainerReference(id.ToString("D"));
            var blob = container.GetBlobReference("index.json");
            using (var ms = new MemoryStream())
            {
                await blob.DownloadToStreamAsync(ms);
                data = ms.ToArray();
                _cache.Set(id, data, TimeSpan.FromSeconds(10));
            }
        }
        return File(data, "application/json");
    }

@kichalla
Copy link
Member

Thanks @nrandell I was able to repro it. As @DamianEdwards mentioned, there should be a check here for response already started or not

@scrouchman
Copy link

Whats the recommended workaround for this? The error is causing my response to end prematurely.
Currently I'm removing the SaveTempDataAttribute from the filters on startup (The project doesn't use TempData) which works, but not convinced its a good fix

@kichalla
Copy link
Member

@scrouchman Following is a workaround that you can use:

Assuming that you are hitting this exception when you are trying to write to the response stream directly in an action for example, you can do the following:

class StreamActionResult : IActionResult
{
    private readonly Func<HttpResponse, Task> _writeToResponseFunc;

    public StreamActionResult(Func<HttpResponse, Task> writeToResponseFunc)
    {
        if(writeToResponseFunc == null)
        {
            throw new ArgumentNullException(nameof(writeToResponseFunc));
        }

        _writeToResponseFunc = writeToResponseFunc;
    }

    public Task ExecuteResultAsync(ActionContext context)
    {
        return _writeToResponseFunc(context.HttpContext.Response);
    }
}

Now you can use this in a controller's action like below:

public IActionResult WriteToStreamFromAction()
{
    return new StreamActionResult((response) =>
    {
        return response.WriteAsync("Hello World");
    });
}

The reason this works is because the func provided in the action is not executed until the StreamActionResult is executed.

If your scenario is different than my assumption, could you share it?

@scrouchman
Copy link

Thanks for this. Your assumption is mostly right, though I'm writing bytes directly to Response.Body. This new action result does indeed fix the issue and is definitely nicer than removing the filter

@rynowak
Copy link
Member

rynowak commented Dec 30, 2016

@scrouchman you can also safely just return a stream from your action method. Or use ObjectResult and your stream as the payload if you want to set a content type. We have a formatter on by default that will copy a stream's contents to the body.

https://github.com/aspnet/Mvc/search?utf8=%E2%9C%93&q=StreamOutputFormatter

@Eilon
Copy link
Member

Eilon commented Jan 19, 2017

This patch bug is approved. Please use the normal code review process w/ a PR and make sure the fix is in the correct branch, then close the bug and mark it as done.

@ryanbrandenburg
Copy link
Contributor

I discussed this with @rynowak and we think that the correct solution here is to move the OnStarting code into OnResourceExecuting so that the the action wont have a chance to start the Response Body before this handler is added.

@Eilon
Copy link
Member

Eilon commented Jan 20, 2017

Yup, sounds like a good plan.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

7 participants