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:
- Starting a recursive task and sending data (
" "
) every 10 seconds - Using
WhenAny
with 2 tasks; my actual service call andTask.Delay(120000)
- Wait until one of the tasks in step #2 completes and cancel the recursive #1 task.
- 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.