Escaping the Cancel Button Trap: AbortController and CancellationToken in ASP.NET API

Canceling a long-running process from a UI form doesn't mean the server has stopped processing the work. Let's learn how to use AbortController and CancellationTokens to help cancel a task all the way down when building ASP NET applications.

Rahul Pulikkot Nath
Rahul Pulikkot Nath

Table of Contents

Imagine this..

A user starts a resource-heavy task from your app's UI - maybe processing extensive data or running a complex report.

Halfway through, they realize something's wrong and hit the cancel button. The UI responds immediately, showing the task as canceled.

Problem solved, right?

Wrong!!

Canceling a long-running process from a UI form doesn't mean the server has stopped processing the work. Your server could still be chugging along, consuming precious resources for something no one wants anymore.

Depending on the kind of work, this could be more than inefficient—it could also be dangerous and lead to inconsistent states and misleading results.

So, how do you let the user cancel this work and ensure it gets canceled on the server?

Thanks to AWS for sponsoring this post in my ASP NET Series.

The Example Scenario

This post will explore the scenario where a user uploads a file to Amazon S3 and has the option to cancel the operation midway.

Depending on your use case, this could be replaced with any long-running process that can be canceled based on user input from the UI.

A simple UI form that allows a user to upload files to Amazon S3 using an API endpoint.

Amazon Simple Storage Service (S3) is an object storage service that provides a scalable and secure storage infrastructure.

Amazon S3 For the .NET Developer: How to Easily Get Started
Learn how to get started with Amazon S3 from a .NET Core application. We will learn how to store and retrieve data from the storage, important concepts to be aware of while using S3.

The Cancel Button Trap on the UI

The UI shown above calls the below POST /upload-large-file endpoint, when the Upload button is clicked after selecting a file to upload.

The code uses the .NET Amazon S3 SDK to upload the file to S3 storage.

app.MapPost("/upload-large-file", async (
        [FromForm] FileUploadRequest request, 
        CancellationToken cancellationToken) =>
    {
        try
        {
            var s3Client = new AmazonS3Client();
            await s3Client.PutObjectAsync(new PutObjectRequest()
            {
                BucketName = "user-service-large-messages",
                Key = $"{Guid.NewGuid()} - {request.File.FileName}",
                InputStream = request.File.OpenReadStream()
            }, cancellationToken);
            await PerformAdditionalTasks(CancellationToken.None);
            return Results.Ok();
        }
        catch (OperationCanceledException e)
        {
            return Results.StatusCode(499);
        }
    })
    .WithName("UploadLargeFile")
    .DisableAntiforgery()
    .WithOpenApi();

The HTTP Endpoint takes in a CancellationToken as part of its parameters.

In ASP NET APIs, when a user makes an HTTP request, the framework automatically creates a CancellationTokenSource and passes the token along with the HttpContext as the ReuestAborted property.

We explored this previously when we learned about Cancellation Tokens in .NET.

A .NET Programmer’s Guide to CancellationToken
Imagine having a long-running request triggered by a user on your server. But the user is no longer interested in the result and has navigated away from the page. However, the server is still processing that request and utilizing resources until you come along and implement Cancellation Tokens in the

When the file uploaded is large, it takes time to upload it to S3.

If the user cancels the operation midway during the upload to S3 using the Cancel button, the underlying API request is not automatically cancelled.

const response = await fetch('https://localhost:44367/upload-large
        method: 'POST',
        body: formData
    });

The file will continue to be uploaded to S3, and additional steps will be performed.

Since the UI no longer cares about this request, it ignores the response from the request.

⚠️
Depending on the kind of work, this could be more than inefficient—it could also be dangerous and lead to inconsistent states and misleading results.

Abort Controllers For Cancelling .NET API Request

In these scenarios, when we need to cancel an HTTP response from a UI form/page, we can use AbortControllers to signal this cancellation to the underlying HTTP requests.

The AbortController interface represents a controller object that allows you to abort one or more Web requests as and when desired.

We pass in the AbortSignal as a request option whenever a fetch request is made. This allows us to invoke the Abort function on the AbortController and also have the associated fetch request aborted.

abortController = new AbortController();
try {
    const response = await fetch('https://localhost:44367/upload-large
        method: 'POST',
        body: formData,
        signal: abortController.signal
    });
    ...
} catch (error) {
    if (error.name === 'AbortError') {
        userMessage = 'Upload cancelled.';
    }
  ...
}

...

cancelBtn.addEventListener('click', () => {
    abortController.abort();
    ...
});

When abort() is called, the fetch throws DOMException named AbortError.

In the above case, I'm explicitly handling this and showing the user the message that the operation is canceled.

Since the fetch request also listens to the abort signal, it also invokes a cancellation of the request.

This will trigger the CancellationToken wired up for the specific HTTP request, which, in turn, cancels the upload to the S3 bucket.

The file will not be uploaded in this scenario to S3.

There's still the possibility of the user canceling the upload request right after the file finished uploading to S3. In these scenarios, we will need to write additional clean-up code to remove the files from S3 and roll back the operation as a whole.

Alternatively, we can move to message-based patterns to avoid doing long-processing tasks during the HTTP request/response cycle.

Dotnet