Home > Mobile >  Writing to http response stream while waiting on a Task to complete?
Writing to http response stream while waiting on a Task to complete?

Time:10-19

I'm calling some long-running services and using Azure Relay to access them. The issue is Azure Relay requires a response within 60 seconds or I get a 504 timeout. I do want to have an actual timeout (2 min).

I'm attempting to solve this with 3 tasks by:

  1. Starting a recursive task and sending data (" ") every 10 seconds
  2. Using WhenAny with 2 tasks; my actual service call and Task.Delay(120000)
  3. Wait until one of the tasks in step #2 completes and cancel the recursive #1 task.
  4. If the completed task is my actual task, then return the results. Otherwise return 504 timeout.

When I test it with <10 seconds, things work fine. If I test it with > 10 seconds, I'm getting:

The HTTP status code, status description, and headers cannot be modified after writing to the output stream.

I'm new to this and I think there are some fundamental things wrong with my code and that error is just a product of that.

[HttpGet(Name = "GetNameAndDateTimeDelayKeepAlive")]
public async Task<ActionResult<string>> GetNameAndDateTimeDelayKeepAlive([FromQuery, DefaultValue(20)] int seconds)
{
    var cts = new CancellationTokenSource();

    var mainTask = _client.GetNameAndDateTimeDelayKeepAlive(seconds);

    // Start a recursive task that will send a " " every 10 seconds to the response stream
    var keepAliveTask = Task.Run(() => KeepAliveLoop(cts.Token));

    // Arbitrary 2 min MAX timeout
    var response = await Task.WhenAny(mainTask, Task.Delay(120000)).ConfigureAwait(true);

    // Cancel the keep alive loop
    cts.Cancel();

    // Task completed within the alloted timeout
    if (response == mainTask)
    {
        return Ok(mainTask.Result.response);
    }

    // If we're here then task didn't complete in 2 min timeout
    return new ObjectResult(this) { Value = "504 Gateway Timeout", StatusCode = 504 };
}

protected async Task KeepAliveLoop(CancellationToken ct)
{
    if (!ct.IsCancellationRequested)
    {
        // Wait 10 seconds
        await Task.Delay(10000);

        // Write " " to the response stream to keep connection alive
        StreamWriter sw;
        await using ((sw = new StreamWriter(Response.Body)).ConfigureAwait(false))
        {
            await sw.WriteLineAsync(" ").ConfigureAwait(false);
            await sw.FlushAsync().ConfigureAwait(false);
        }

        // Call self to loop
        await KeepAliveLoop(ct);
    }
}

CodePudding user response:

You can't combine this kind of streaming with ActionResult.

ActionResult is a complete HTTP response, including status code, headers, and body. Streaming, though (i.e., writing to Response.Body) is done by writing the body. In order to write the body, the status code / headers must all already be sent.

So, as soon as your code starts writing to Response.Body, ASP.NET writes out the status code / headers. Then when your code returns ActionResult, it attempts to write out the status code, headers, and body; and you get the exception saying the status code / headers have already been written.

The only solution I know of is to not use ActionResult (or Ok or ObjectResult) in a method that writes to Response.Body. You'll have to do your own serialization of your response and write that to Response.Body instead.

  • Related