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.

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.

Appendix: 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)