dotnet/yarp · warning · OperationCanceledException

The request body copy was canceled.

Error message

The request body copy was canceled.

What it means

During the client-to-destination body copy, the operation is canceled (typically by the activity timeout firing or the request being aborted). The `StreamCopier.CopyAsync` returns `StreamCopyResult.Canceled` and YARP wraps the cancellation into an `OperationCanceledException` so the HttpClient transport aborts the outgoing request body. This is the normal path for timed-out or aborted streaming requests, not necessarily a bug.

Source

Thrown at src/ReverseProxy/Forwarder/StreamCopyHttpContent.cs:206

            // and we are deliberately unaware of packet structure used e.g. in gRPC duplex channels.
            // Because the sockets aren't flushed, the perf impact of this choice is expected to be small.
            // Future: It may be wise to set this to true for *all* http2 incoming requests,
            // but for now, out of an abundance of caution, we only do it for requests that look like gRPC.
            var (result, error) = await StreamCopier.CopyAsync(isRequest: true, _context.Request.Body, stream,
                Headers.ContentLength ?? StreamCopier.UnknownLength, _timeProvider, _activityToken, _isStreamingRequest, cancellationToken);
            _tcs.TrySetResult((result, error));

            // Check for errors that weren't the result of the destination failing.
            // We have to throw something here so the transport knows the body is incomplete.
            // We can't re-throw the original exception since that would cause concurrency issues.
            // We need to wrap it.
            if (result == StreamCopyResult.InputError)
            {
                throw new IOException("An error occurred when reading the request body from the client.", error);
            }
            if (result == StreamCopyResult.Canceled)
            {
                throw new OperationCanceledException("The request body copy was canceled.", error);
            }
        }
        finally
        {
            linkedCts?.Dispose();
        }
    }

    // this is used internally by HttpContent.ReadAsStreamAsync(...)
    protected override Task<Stream> CreateContentReadStreamAsync()
    {
        // Nobody should be calling this...
        throw new NotImplementedException();
    }

    protected override bool TryComputeLength(out long length)
    {
        // We can't know the length of the content being pushed to the output stream.

View on GitHub (pinned to bd11867bee)

Solutions

  1. If the timeout is expected for your workload (large uploads, gRPC streaming), increase `ActivityTimeout` in the cluster's `HttpRequest` config or the `ForwarderRequestConfig`.
  2. For gRPC and long-lived streaming connections, set the activity timeout to `System.Threading.Timeout.InfiniteTimeSpan` to disable it.
  3. Verify the client is not intentionally stalling — investigate client-side network issues if cancellations are unexpected.
  4. Handle the `OperationCanceledException` in your error pipeline and return 504 Gateway Timeout or 499 (Client Closed Request) as appropriate.
  5. Check that no upstream middleware or client is aborting the request prematurely via `context.Abort()`.

Example fix

// before — default 100s timeout may cancel slow uploads
var config = ForwarderRequestConfig.Empty;
// after — extended timeout for large-file-upload or gRPC streaming routes
var config = new ForwarderRequestConfig
{
    ActivityTimeout = TimeSpan.FromMinutes(10)
};
// or for gRPC/streaming with no idle timeout
var grpcConfig = new ForwarderRequestConfig
{
    ActivityTimeout = Timeout.InfiniteTimeSpan
};
Defensive patterns

Strategy: try-catch

Validate before calling

// Ensure ActivityTimeout is appropriate for the workload type
var timeout = requestConfig?.ActivityTimeout ?? TimeSpan.FromSeconds(100);
if (isStreamingRoute && timeout < TimeSpan.FromMinutes(5))
{
    logger.LogWarning("Activity timeout {Timeout} may be too short for streaming routes", timeout);
    requestConfig = requestConfig with { ActivityTimeout = Timeout.InfiniteTimeSpan };
}

Type guard

// No type guard — cancellation is a runtime condition, not a type issue.

Try / catch

try { await forwarder.SendAsync(context, prefix, client, config, transformer, ct); }
catch (OperationCanceledException ex) when (ex.Message.Contains("request body copy was canceled"))
{
    if (!context.Response.HasStarted)
    {
        context.Response.StatusCode = context.RequestAborted.IsCancellationRequested
            ? 499  // Client Closed Request (nginx convention)
            : StatusCodes.Status504GatewayTimeout;
    }
}

Prevention

When it happens

Trigger: The YARP activity timeout (default 100 seconds) elapses with no forward progress while streaming the request body (the client is too slow or stalled). Or `context.RequestAborted` fires because the client disconnected, which propagates through the linked CancellationTokenSource. The copy returns `Canceled` and line 206 throws the wrapped exception.

Common situations: A client streams a large upload very slowly (e.g., throttled mobile upload) and exceeds the 100-second activity timeout. A gRPC streaming call stalls. A client disconnects mid-upload, triggering RequestAborted. The activity timeout is set too aggressively for the workload.

Related errors


AI-assisted analysis of dotnet/yarp@bd11867bee (2026-08-13). Data as JSON: /api/errors/695225ba28257d0e. Report an issue: GitHub.