Cancelling a request issues a cancellation on backend?

coderethinked.com: cancelling request issues cancellation

In the previous post, we saw how to cancel a request using axios in VueJS. So, does cancelling a request really issues a cancellation token to the backend API or does cancelling a request just cancels on the UI only? Let’s find out.

Preparation

Here is what I’ll do for this post

  1. Create a new backend API
  2. Invoke the backend API from UI with cancellation token

As a .NET developer, I know .NET framework has CancellationToken implementation. So, for this post, I’ll create a new .NET Core app and add a new controller.

Creating .Net Core API

So, here is my new controller.

[HttpGet("users")]
public async Task<List<User>> GetUsersAsync(CancellationToken cancellationToken)
{
    try
    {
        await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken);
        return new List<User>
        {
            new User {Id = 1, Name = "Karthik"},
            new User {Id = 2, Name = "Rithvik"}
        };
    }
    catch (OperationCanceledException canceledException) when (cancellationToken.IsCancellationRequested)
    {
        Console.WriteLine(canceledException);
        throw;
    }
    
}

So, when the operation is cancelled, I’ll log the exception to the console.

And if we issue a cancellation token to the endpoint while the Delay is in progress, we will fall into the exception as cancellationToken is requested.

Running the test

I’m using the code from my previous post as we have “Send Request” and “Cancel request” buttons in the UI and all we’ve to change is the API endpoint to get this started.

Here’s the UI code anyway.

<template>
  <div>
    <div>
      <b-button variant="outline-success" @click="send">Send Request</b-button>
       
      <b-button
        variant="outline-danger"
        :disabled="!requestInProgress"
        @click="cancelRequest"
        >Cancel Request</b-button
      >
       
      <b-button variant="outline-info" @click="reset">Reset</b-button>
    </div>
    <br />
    <p>Request Status: {{ currentState }}</p>
    <div v-if="users.length > 0">
      <div>
        <h4>Users in the reqres.in</h4>
      </div>
      <b-table
        striped
        bordered
        responsive
        hover
        :items="users"
        :fields="fields"
      >
        <template #cell(avatar)="data">
          <img v-bind:src="data.value" :width="50" :height="50" />
        </template>
      </b-table>
    </div>
  </div>
</template>

<script>
import { BTable, BButton } from "bootstrap-vue";
import axios from "axios";

const states = {
  IDLE: "Idle 🟡",
  IN_PROGRESS: "In Progress 📀",
  SUCCEEDED: "Successful ✅",
  CANCELLED: "Cancelled ❌",
};

// const API_URL = "https://reqres.in/api/users?delay=2";

export default {
  name: "User",
  components: {
    BTable,
    BButton,
  },
  data() {
    return {
      fields: ["id", "avatar", "first_name", "last_name", "email"],
      users: [],
      request: null,
      currentState: states.IDLE,
    };
  },
  methods: {
    cancelRequest() {
      this.cancel();
      this.updateState(states.CANCELLED);
    },
    send() {
      this.users = [];
      this.updateState(states.IN_PROGRESS);
      this.cancel();
      let axiosSource = axios.CancelToken.source();
      this.request = { cancel: axiosSource.cancel };
      axios
        .get("http://localhost:5000/users", {
          cancelToken: axiosSource.token,
        })
        .then((res) => {
          this.users = res.data.data;
          this.updateState(states.SUCCEEDED);
        });
    },
    updateState(msg) {
      this.currentState = msg;
      this.request = null;
    },
    reset() {
      this.currentState = states.IDLE;
      this.cancel();
      this.request = null;
      this.users = [];
    },
    cancel() {
      if (this.request) this.request.cancel();
    },
  },
  computed: {
    requestInProgress() {
      return this.request;
    },
  },
};
</script>

Let’s run both the UI and .net core backend. And here is a demo of it.

And we got our cancellation exception logged when we cancelled the request.

Okay, this looks good. But, let’s see how we can implement the cancellationToken for database operation

Without cancellationToken for database operations

Before we see the cancellation code, let’s see what happens if we don’t use cancellation token.

[HttpGet("users")]
public async Task<List<User>> GetUsersAsync(CancellationToken cancellationToken)
{
     await Task.Delay(TimeSpan.FromSeconds(5));
    var users = GetUsersFromDbWithoutCancelAsync();
    return await users;
}

private async Task<List<User>> GetUsersFromDbWithoutCancelAsync()
{
    var users = new List<User>();

    var cmd = this.MySqlDatabase.Connection.CreateCommand() as MySqlCommand;
    cmd.CommandText = "SELECT * from users";
    cmd.CommandType = System.Data.CommandType.Text;

    try
    {
        using (var reader = (MySqlDataReader)await cmd.ExecuteReaderAsync())
        {
            if (!reader.HasRows) return null;
            while (await reader.ReadAsync())
            {
                users.Add(new User { Id = reader.GetFieldValue<int>(0), Name = reader.GetFieldValue<string>(1) });
            }
        }
    }
    catch (Exception ex) 
    {
        Debug.WriteLine(users);
        Debug.WriteLine(ex);

        throw;
    }

    return users;
}

I’m using MySQL package for reading the data from database. The GetUsersFromDbWithoutCancelAsync just returns all the users in our users table.

Here’s the demo of it.

With cancellationToken for database operations

Now, let’s see what would happen if we cancel in the middle of a database operation.

I’ve modified the GetUsersFromDbAsync method to use the cancellationToken. And here it is.

private async Task<List<User>> GetUsersFromDbAsync(CancellationToken cancellationToken)
{
    var users = new List<User>();

    var cmd = this.MySqlDatabase.Connection.CreateCommand() as MySqlCommand;
    cmd.CommandText = "SELECT * from users";
    cmd.CommandType = System.Data.CommandType.Text;

    try
    {
        using (var reader = (MySqlDataReader)await cmd.ExecuteReaderAsync(cancellationToken))
        {
            if (!reader.HasRows) return null;
            while (await reader.ReadAsync(cancellationToken))
            {
                users.Add(new User { Id = reader.GetFieldValue<int>(0), Name = reader.GetFieldValue<string>(1) });
            }
        }
    }
    catch (OperationCanceledException ex) when (cancellationToken.IsCancellationRequested)
    {
        Console.WriteLine(users);
        Console.WriteLine(ex);

        throw;
    }
    
    return users;
}

We passed cancellation token to the ExecuteReaderAsync and ReadAsync method to be able to cancel the operation if cancellation is requested at the time of fetching from database.

And in the catch block, when a cancellation is requested, I’ll write all the users fetched to console until cancellation is requested.

Let’s see this in action.

with cancellation token

When we cancelled the operation in UI, we got cancellationToken as true in the C# and we logged the data we have to console until the operation is cancelled.

So, cancelling a request cancels operations on backend?

Yes, it does. But, the libraries should also support cancellation operation.

For this post, I’ve used MySQL library to connect to the database. The library method, for example ReadAsync, supports cancellation and we are able to cancel the operation.

References

  1. Creating Web API’s in ASP.NET Core
  2. Cancellation in vuejs