@cancjs/axios
v1.0.0
Published
Cancelable promise adapter for axios.
Maintainers
Readme
Introduction
A drop-in replacement for the axios default export whose request methods return a
CancelablePromise. Calling .cancel() aborts the request and rejects with a CancelError, so
cancellation reads as an ordinary rejection in try/catch.
The wrapper holds no state of its own. It forwards to a real axios instance, so config merging,
header handling, interceptor chains and create() seeding are still axios's own code.
Features
- same call shapes and signatures as axios, including the full
AxiosResponseresult .cancel()aborts the in-flight request through an AbortSignal- cancellation is supported through the full request lifecycle, including interceptors
- a caller-supplied
config.signalalso rejects with aCancelError - instances from
create()are wrapped too, with their own defaults and interceptors - works with the xhr, http and fetch adapters, and with axios 0.22 and up
- no Proxy, ES5-friendly output
Getting Started
Installation
npm install @cancjs/axios axios @cancjs/promiseaxios (0.22 or later) and @cancjs/promise are peer dependencies. This package is ecosystem
tier: a minor release can carry a breaking change, so pin it with a tilde, ~1.4 (pin the minor, not
~1.x, which npm expands to the same range as ^1), rather than the default caret. See
Versioning for the full policy.
Usage
import cancelableAxios from '@cancjs/axios';
import { isCancelError } from '@cancjs/promise';
const promise = cancelableAxios.get('/issues', { params: { q: 'bug' } });
promise.cancel('superseded');
try {
const response = await promise;
console.log(response.data);
} catch (error) {
if (isCancelError(error)) {
// the request was aborted
}
}An instance keeps its own defaults and interceptors, exactly like axios.create():
const api = cancelableAxios.create({ baseURL: 'https://api.example.com' });
api.defaults.headers.common['Authorization'] = 'Bearer token';The default export and the named cancelableAxios export are the same binding, so
import cancelableAxios from '@cancjs/axios' and import { cancelableAxios } from '@cancjs/axios'
give you the identical object. Pick whichever reads better next to your existing axios import.
An axios instance built elsewhere can be wrapped instead:
import axios from 'axios';
import { cancelableAxios } from '@cancjs/axios';
const axiosInstance = axios.create({ baseURL: 'https://api.example.com' });
const api = cancelableAxios.wrap(axiosInstance);In a coroutine the request joins the surrounding cancellation:
import * as canc from '@cancjs/coroutine';
const loadIssues = canc.async(function* (query: string) {
const response = yield* canc.await(
api.get('/issues', { params: { q: query } })
);
return response.data;
});
const pending = loadIssues('bug');
pending.cancel(); // aborts the requestHow It Works
Each request method (get, post, put and the rest) creates its own AbortController, merges
the signal into the request config, and returns a CancelablePromise. Canceling the promise
aborts that controller, so axios handles the transport-level abort through whichever adapter it is
using (xhr, http or fetch).
Cancellation is supported through the full lifecycle of a request, not just the network call. When a request is canceled, interceptors that are still running are canceled too. An interceptor that returns a cancelable promise has that promise canceled along with the request, so work started in an interceptor (a token refresh, a retry) does not keep going after the caller has walked away.
An existing config.signal from the caller is composed with the internal one. Either source can
abort the request, and in both cases the promise rejects with a CancelError, normalizing the
error regardless of which signal aborted.
The wrapper holds no state beyond what axios itself holds. defaults on the wrapper is a live
accessor onto the underlying instance's defaults, and interceptors is a facade over the real
interceptor managers, so IDs stay valid and interceptors added directly on the underlying instance
still run. create() returns another wrapped instance with its own defaults and interceptors, and
wrap() wraps an existing axios instance without creating a new one.
Description
Interceptors
Interceptors receive a second argument carrying the cancellation context of the request they run
for. The context exposes signal, isCanceled(), cancel() and link(promise):
api.interceptors.request.use((config, ctx) => {
if (ctx.isCanceled()) {
return config;
}
return refreshToken({ signal: ctx.signal }).then((token) => {
config.headers.Authorization = `Bearer ${token}`;
return config;
});
});A cancelable promise returned from an interceptor is canceled along with the request. In the
example above, if the request is canceled while the token refresh is in flight, the refresh
promise is canceled too. ctx.link(promise) explicitly ties any cancelable promise to the
request's lifecycle, so work started outside the return path is still canceled with the request.
Combinators
all differs from axios.all on purpose: it builds a CancelablePromise, so canceling one
request rejects the aggregate and cancels the rest. spread is unchanged.
Accessing the underlying instance
The wrapped instance is reachable as .axios when a plain native promise is needed, for example
when handing a request to code that does not understand cancellation.
API
cancelableAxios is both the default export and a named export (both references point to the same instance). It mirrors the full axios
interface: request, get, delete, head, options, post, put, patch, getUri,
create, defaults, interceptors, all, spread.
cancelableAxios.create(config?) returns a new wrapped instance.
cancelableAxios.wrap(axiosInstance) and wrapAxios(axiosInstance) wrap an existing axios instance. The argument is
structurally typed, so it accepts instances from any axios version without type conflicts.
CancelScope is the per-request cancellation scope class managing request lifecycle and interceptor promises.
.axios on any wrapped instance returns the underlying axios instance.
Members axios added after 0.22 (postForm, putForm, patchForm, AxiosHeaders, HttpStatusCode and the rest) plus standard axios exports (AxiosError, CancelToken, CanceledError, isAxiosError, VERSION, formToJSON, getAdapter, mergeConfig, toFormData) are re-exported.
Compatibility
Axios 0.22 and later, which is when signal support was added. Node.js 18 and later, current
browsers. Everything else follows
@cancjs/promise.
Documentation
@cancjs/promisefor the cancellation model- Coroutines for using axios inside a cancelable flow
- Examples:
app-axiosfor a side-by-side comparison against a manual request registry - Repository for the ecosystem overview
Contributing
You are welcome to participate through issues and pull requests!
