bhylu.com

Advertisements

AngularJS Request Cancellation

A newer search can make an older response irrelevant. A cancellation promise can abort a pending $http request, while a generation check prevents an obsolete callback from becoming the current result.

How it works

The fixture provides one slow search and one fast search. Each new operation cancels the previous one and increments a local serial value. The callback updates the view only when its serial still matches. Destroying the controller’s scope cancels the remaining request too. Cancellation affects the client request; it does not prove a server reversed an already committed operation.

Run the code

AngularJS 1.8.3 is a legacy framework whose official support ended in January 2022. These examples teach maintenance, not a recommendation for a new application. They need a separate local browser page; Bhylu has no AngularJS playground.

In an empty folder save the HTML below as request-cancellation.html and the server as lab-server.mjs. Make a vendor folder and save the four pinned libraries there, retaining their MIT notices. With Node 22 or later run node lab-server.mjs request-cancellation.html, then open http://127.0.0.1:8136/. Stop that lab before running a different lesson. The server supplies fixed read-only HTTP fixtures; no data is saved and it does not connect to SQL. No PHP, Apache or MySQL configuration changes are needed.

Example

request-cancellation.html

<!doctype html>
<html lang="en" ng-app="repairLab" ng-strict-di>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <title>AngularJS Request Cancellation</title>
    <style>
      :root {
        font:
          18px/1.55 system-ui,
          sans-serif;
        color: #0f3464;
        background: #fff;
        color-scheme: light;
      }
      * {
        box-sizing: border-box;
      }
      body {
        margin: 0;
      }
      main {
        max-width: 50rem;
        margin: auto;
        padding: 1.2rem;
      }
      h1 {
        font-size: 1.65rem;
      }
      label {
        display: block;
        margin: 0.6rem 0;
      }
      input,
      select,
      textarea,
      button {
        font: inherit;
        max-width: 100%;
        padding: 0.35rem;
        margin: 0.2rem;
      }
      button {
        border: 1px solid #034fa0;
        border-radius: 0.25rem;
        background: #034fa0;
        color: white;
      }
      button:disabled {
        opacity: 0.55;
      }
      output,
      [role="status"] {
        display: block;
        min-height: 1.5rem;
      }
      table {
        border-collapse: collapse;
        width: 100%;
      }
      td,
      th {
        padding: 0.4rem;
        border: 1px solid #a5bbd4;
        text-align: left;
      }
      pre {
        white-space: pre-wrap;
        overflow-wrap: anywhere;
      }
      :focus-visible {
        outline: 3px solid #0078d4;
        outline-offset: 3px;
      }
      [ng-cloak] {
        display: none !important;
      }
      .marked {
        font-weight: 700;
      }
      .error {
        color: #a02424;
      }
      :root[data-theme="dark"] {
        color: #d6e7fb;
        background: #061d30;
        color-scheme: dark;
      }
      :root[data-theme="dark"] .error {
        color: #ffb4b4;
      }
    </style>
  </head>
  <body>
    <main ng-controller="DeskController as vm" ng-cloak>
      <h1>Community repair desk</h1>
      <p>AngularJS 1.8.3 · Legacy maintenance lab · Local demonstration</p>
      <button type="button" data-slow ng-click="vm.search('slow')">
        Slow search</button
      ><button type="button" data-fast ng-click="vm.search('fast')">
        Fast search
      </button>
      <p data-result>{{ vm.message }}</p>
    </main>
    <script src="/vendor/angular.min.js"></script>
    <script>
      (function () {
        "use strict";
        var app = angular.module("repairLab", []);
        app.controller("DeskController", [
          "$http",
          "$q",
          "$scope",
          function ($http, $q, $scope) {
            var vm = this,
              cancel = null,
              serial = 0;
            vm.message = "No search";
            vm.search = function (query) {
              var run = ++serial;
              if (cancel) cancel.resolve();
              cancel = $q.defer();
              return $http
                .get("/api/search", {
                  params: { q: query },
                  timeout: cancel.promise,
                })
                .then(
                  function (response) {
                    if (run === serial) vm.message = response.data.label;
                  },
                  function (response) {
                    if (run === serial && response.status !== -1)
                      vm.message = "Search failed";
                  },
                );
            };
            $scope.$on("$destroy", function () {
              serial += 1;
              if (cancel) cancel.resolve();
            });
          },
        ]);
      })();
    </script>
  </body>
</html>

Focus on the bindings and controller

This excerpt leaves out the document shell, styles and library files. Use the complete download to run it.


<button type="button" data-slow ng-click="vm.search('slow')">Slow search</button
><button type="button" data-fast ng-click="vm.search('fast')">
  Fast search
</button>
<p data-result>{{ vm.message }}</p>
<script>
  (function () {
    "use strict";
    var app = angular.module("repairLab", []);
    app.controller("DeskController", [
      "$http",
      "$q",
      "$scope",
      function ($http, $q, $scope) {
        var vm = this,
          cancel = null,
          serial = 0;
        vm.message = "No search";
        vm.search = function (query) {
          var run = ++serial;
          if (cancel) cancel.resolve();
          cancel = $q.defer();
          return $http
            .get("/api/search", {
              params: { q: query },
              timeout: cancel.promise,
            })
            .then(
              function (response) {
                if (run === serial) vm.message = response.data.label;
              },
              function (response) {
                if (run === serial && response.status !== -1)
                  vm.message = "Search failed";
              },
            );
        };
        $scope.$on("$destroy", function () {
          serial += 1;
          if (cancel) cancel.resolve();
        });
      },
    ]);
  })();
</script>

Expected result

No search

The browser starts the slow request, replaces it with the fast one and checks that Fast repair result remains after the slow delay. The unit fixture checks the controller’s cancellation and request lifecycle through AngularJS’s own backend mock rather than a hand-written imitation.

Advertisements