Important: This documentation covers Yarn 1 (Classic).
For Yarn 2+ docs and migration guide, see yarnpkg.com.

Package detail

xior

suhaotian37.6kMIT0.7.8TypeScript support: included

A lite http request lib based on fetch with plugins support and similar API to axios.

fetch, fetch wrapper, axios, axios fetch, axios alternatives, xior, fetch plugins, tauri, plugins, http, edge runtime, https, network, url, uri, mock, dedupe, promise, request, error retry, request cache, request throttle

readme

Build Size npm version Downloads typescript

Intro

A lite http request lib based on fetch with plugin support and similar API to axios.

Features:

  • 🔥 Use fetch
  • 🫡 Similar axios API: axios.create / axios.interceptors / .get/post/put/patch/delete/head/options
  • 🤙 Supports timeout, canceling requests, and nested query encoding
  • 🥷 Supports plugins: error retry, deduplication, throttling, cache, error cache, mock, and custom plugins
  • 🚀 Lightweight (~6KB, Gzip ~3kb)
  • 👊 Unit tested and strongly typed 💪

Table of Contents

Getting Started

Installing

Package manager

# npm
npm install xior

# pnpm
pnpm add xior

# bun
bun add xior

# yarn
yarn add xior

Use CDN

Since v0.2.1, xior supports UMD format

Use jsDelivr CDN:

<script src="https://cdn.jsdelivr.net/npm/xior@0.7.8/dist/xior.umd.js"></script>

<!-- Usage -->
<script>
  console.log(xior.VERSION);

  xior.get('https://exmapledomain.com/api').then((res) => {
    console.log(res.data);
  });
</script>

Use unpkg CDN:

<script src="https://unpkg.com/xior@0.7.8/dist/xior.umd.js"></script>

<!-- Usage -->
<script>
  xior.get('https://exmapledomain.com/api').then((res) => {
    console.log(res.data);
  });
</script>

Create instance

import xior from 'xior';

export const xiorInstance = xior.create({
  baseURL: 'https://apiexampledomain.com/api',
  headers: {
    // put your common custom headers here
  },
});

GET / POST / DELETE / PUT / PATCH / OPTIONS / HEAD

GET

HEAD / DELETE / OPTIONS are same usage with GET method

async function run() {
  const { data } = await xiorInstance.get('/');

  // with params and support nested params
  const { data: data2 } = await xiorInstance.get('/', { params: { a: 1, b: 2, c: { d: 1 } } });

  // with headers
  const { data: data3 } = await xiorInstance.get('/', {
    params: { a: 1, b: 2 },
    headers: {
      'content-type': 'application/x-www-form-urlencoded',
    },
  });

  // types
  const { data: data4 } = await xiorInstance.get<{ field1: string; field2: number }>('/');
}

POST

PUT/PATCH methods are same usage with POST

async function run() {
  const { data: data3 } = await xiorInstance.post<{ field1: string; field2: number }>(
    '/',
    { a: 1, b: '2' },
    {
      params: { id: 1 },
      headers: {},
    }
  );
}

Change default headers or params

import xior from 'xior';

export const xiorInstance = xior.create({
  baseURL: 'https://apiexampledomain.com/api',
});

function setAccessToken(token: string) {
  // xiorInstance.defaults.params['x'] = 1;
  xiorInstance.defaults.headers['Authorization'] = `Bearer ${token}`;
}

function removeUserToken() {
  // delete xiorInstance.defaults.params['x'];
  delete xiorInstance.defaults.headers['Authorization'];
}

Get response headers

import xior from 'xior';

const xiorInstance = xior.create({
  baseURL: 'https://apiexampledomain.com/api',
});

const { data, headers } = await xiorInstance.get('/');

console.log(headers.get('X-Header-Name'));

Upload file

xior supports file uploads using the FormData API and provides an optional 'xior/plugins/progress' plugin for simulating upload progress, usage similar to Axios.

import Xior from 'xior';
import uploadDownloadProgressPlugin from 'xior/plugins/progress';

const http = Xior.create({});

http.plugins.use(
  uploadDownloadProgressPlugin({
    progressDuration: 5 * 1000,
  })
);

const formData = FormData();
formData.append('file', fileObject);
formData.append('field1', 'val1');
formData.append('field2', 'val2');

http.post('/upload', formData, {
  onUploadProgress(e) {
    console.log(`Upload progress: ${e.progress}%`);
  },
  // progressDuration: 10 * 1000
});

Using interceptors

xior supports interceptors similar to Axios, allowing you to modify requests and handle responses programmatically.

Request interceptors:

import xior, { merge } from 'xior';

const http = xior.create({
  // ...options
});

http.interceptors.request.use((config) => {
  const token = localStorage.getItem('REQUEST_TOKEN');
  if (!token) return config;

  return merge(config, {
    headers: {
      Authorization: `Bearer ${token}`,
    },
  });
});

// One more interceptors for request
http.interceptors.request.use((config) => {
  return config;
});

async function getData() {
  const { data } = await http.get('/');
  console.log(data);
  return data;
}

Response interceptors:

import xior, { merge } from 'xior';

const http = xior.create({});

http.interceptors.response.use(
  (result) => {
    const { data, request: config, response: originalResponse } = result;
    return result;
  },
  async (error) => {
    if (error instanceof TypeError) {
      console.log(`Request error:`, error);
    }
    if (error?.response?.status === 401) {
      localStorage.removeItem('REQUEST_TOKEN');
    }
    return Promise.reject(error);
  }
);

async function getData() {
  const { data } = await http.get('/');
  console.log(data);
  return data;
}

Cleanup interceptors

import xior from 'xior';

const http = xior.create({});

// Cleanup request interceptors
const handler1 = http.interceptors.request.use((config) => {
  return config;
});
http.interceptors.request.eject(handler1);
// Cleanup all request interceptors
// http.interceptors.request.clear()

// Cleanup response interceptors
const handler2 = http.interceptors.response.use((res) => {
  return res;
});
http.interceptors.response.eject(handler2);
// Cleanup all response interceptors
// http.interceptors.response.clear()

Timeout and Cancel request

Timeout:

import xior from 'xior';

const instance = xior.create({
  timeout: 120 * 1000, // set default timeout
});

await instance.post(
  'http://httpbin.org',
  {
    a: 1,
    b: 2,
  },
  {
    timeout: 60 * 1000, // override default timeout 120 * 1000
  }
);

Cancel request:

import xior from 'xior';
const instance = xior.create();

const controller = new AbortController();

xiorInstance.get('http://httpbin.org', { signal: controller.signal }).then((res) => {
  console.log(res.data);
});

class CancelRequestError extends Error {}
controller.abort(new CancelRequestError()); // abort request with custom error

Proxy or use custom fetch implementations

See 3. How can I use custom fetch implementation or How to support proxy feature?

Custom data parser

In xior, the default response parser is this:

let data = response.text();
if (data) {
  try {
    data = JSON.parse(data);
  } catch (e) {}
}
return data;

But maybe we don't want to do it this way; instead, we want to parse the data based on the content-type from response's headers. So, we can do it this way:

import axios from 'xior';

const http = Xior.create({
  baseURL,
  responseType: 'custom', // Tell xior no need to parse body
});

// Define content type matchers as [responseMethod, [regexpPatterns]]
const typeMatchers = [
  ['json', [/^application\/.*json$/, /^$/]],
  ['text', [/^text\//, /^image\/svg\+xml$/, /^application\/.*xml$/]],
  // ['arrayBuffer', [/^application\/octet-stream/]],
] as const;

http.interceptors.response.use(
  async (res) => {
    try {
      if (res.config.responseType !== 'custom') return res;

      const { response } = res;
      const headers = response?.headers;
      if (!response || headers.get('Content-Length') === '0') return res;

      const contentType = headers.get('Content-Type')?.split(';')?.[0]?.trim() || '';

      // Find matching response method using the typeMatchers array
      const matchedType = typeMatchers.find(([_, patterns]) =>
        patterns.some((pattern) => pattern.test(contentType))
      );

      if (matchedType) {
        const [method] = matchedType;
        res.data = await response[method]();
      } else {
        console.warn(`Unknown Content-Type: ${contentType}`);
      }

      return res;
    } catch (error) {
      console.error('Interceptor error:', error);
      return Promise.reject(error);
    }
  },
  (error) => Promise.reject(error)
);

Encrypt and Decrypt Example

We can use interceptors easily to handle encrypt/decrypt.

Create encryption.ts:

// encryption.ts
export const SECRET = '&*&*^SDxsdasdas776';

export function encrypt(data: string) {
  return data + '____' + SECRET;
}

export function decrypt(data: string, s?: string) {
  return data.replace('____' + (s || SECRET), '');
}

Create xior-instance.ts:

import xior from 'xior';

import { SECRET, encrypt, decrypt } from './encryption';

export const instance = xior.create();

instance.interceptors.request.use((req) => {
  req.headers['X'] = SECRET;

  if (req.url && req.data) {
    const result = JSON.stringify(req.data);
    const blob = encrypt(result);
    req.data = { blob };
  }

  return req;
});

instance.interceptors.response.use((res) => {
  if (res.request.url && res.data?.blob) {
    res.data = decrypt(res.data.blob);
    try {
      res.data = JSON.parse(res.data);
    } catch (e) {
      console.error(e);
    }
  }
  return res;
});

Check test code in tests/src/tests/encrypt-decrypt/

Tips: Make your SSR(Server-side Rendering) app more stable and faster

How do we achieve this? By using Xior's plugins:

  1. If a GET request fails, allow retries for a second chance at success.
  2. If retries still fail, return cached data (if available) to prevent page crashes or error pages.
  3. Deduplicate GET requests to avoid redundant calls.
  4. Throttle GET requests to control request frequency.
  5. For large data that isn’t needed in real-time (like i18n JSON files), serve cached data first and fetch updates in the background. Example code:
import xior, { XiorError as AxiosError } from 'xior';
import errorRetryPlugin from 'xior/plugins/error-retry';
import dedupePlugin from 'xior/plugins/dedupe';
import throttlePlugin from 'xior/plugins/throttle';
import errorCachePlugin from 'xior/plugins/error-cache';

// Setup
const http = axios.create({
  baseURL: 'http://localhost:3000',
});
http.plugins.use(errorRetryPlugin());
http.plugins.use(errorCachePlugin());
http.plugins.use(dedupePlugin()); // Prevent same GET requests from occurring simultaneously.
http.plugins.use(throttlePlugin()); // Throttle same `GET` request in 1000ms

// 1. If `GET` data error, at least have chance to retry;
// 2. If retry still error, return the cache data(if have) to prevent page crash or show error page;
const res = await http.get('/api/get-data'); // these will retry if have error
if (res.fromCache) {
  console.log(`the data from cahce`, res.cacheTime);
}

// 3. Dedupe the same `GET` requests, this will only sent 1 real request
await Promise.all([
  http.get('/api/get-data-2'),
  http.get('/api/get-data-2'),
  http.get('/api/get-data-2'),
]);

// 4. Throttle the `GET` requests,
//    we want throttle some larget data request in 10s, default is 1s
http.get('/api/get-some-big-data', { threshold: 10e3 });

// 5. If have cache data, return the cache data first,
//    and run the real request in background
http.get('/api/get-some-big-data', { threshold: 10e3, useCacheFirst: true });

Plugins

xior offers a variety of built-in plugins to enhance its functionality:

Usage:

import xior from 'xior';
import errorRetryPlugin from 'xior/plugins/error-retry';
import throttlePlugin from 'xior/plugins/throttle';
import cachePlugin from 'xior/plugins/cache';
import uploadDownloadProgressPlugin from 'xior/plugins/progress';

const http = xior.create();

http.plugins.use(errorRetryPlugin());
http.plugins.use(throttlePlugin());
http.plugins.use(cachePlugin());
http.plugins.use(uploadDownloadProgressPlugin());

Error retry plugin

Retry the failed request with special times

API:

function errorRetryPlugin(options: {
  retryTimes?: number;
  retryInterval?: number | ((errorCount: number) => number);
  enableRetry?: boolean | (config: XiorRequestConfig, error: XiorError | Error) => boolean | undefined;
  onRetry?: (config: XiorRequestConfig, error: XiorError | Error, count: number) => void;
}): XiorPlugin;

The options object:

Param Type Default value Description
retryTimes number 2 Set the retry times for failed request
retryInterval number | ((errorCount: number, config: XiorRequestConfig, error: XiorError) => number) 3000 After first time retry, the next retries interval time, default interval is 3 seconds; you can use function as param to return interval number too
enableRetry boolean | ((config: XiorRequestConfig, error: XiorError | Error) => boolean | undefined) (config, error) => config.method === 'GET' || config.isGet Default only retry if GET request error and retryTimes > 0
onRetry boolean | ((config: XiorRequestConfig, error: XiorError | Error, count: number) => void) undefined For log retry info

Basic usage:

import xior from 'xior';
import errorRetryPlugin from 'xior/plugins/error-retry';

const http = xior.create();
http.plugins.use(
  errorRetryPlugin({
    retryTimes: 3,
    // retryInterval: 3000,
    retryInterval(count, config, error) {
      // if (error.response?.status === 500) return 10e3;
      return count * 1e3;
    },
    onRetry(config, error, count) {
      console.log(`${config.method} ${config.url} retry ${count} times`);
    },
    // enableRetry(config, error) {
    //   if ([401, 400].includes(error.response?.status)) { // no retry when status is 400 or 401
    //     return false;
    //   }
    //   // no return or return `undefined` here, will reuse the default `enableRetry` logic
    // },
  })
);

// if request error, max retry 3 times until success
http.get('/api1');

// if request error, will not retry, because `retryTimes: 0`
http.get('/api2', { retryTimes: 0 });

// if POST request error, will not retry
http.post('/api1');

// Use `enableRetry: true` to support post method, max retry 5 times until success
http.post('/api1', null, { retryTimes: 5, enableRetry: true });

Advance usage:

The retry key for the unique request generated by use params and data, if your request depends on headers, you can add request interceptor to add headers's value to params:

import xior from 'xior';
import errorRetryPlugin from 'xior/plugins/error-retry';

const http = xior.create();
http.plugins.use(errorRetryPlugin());

http.interceptors.request.use((config) => {
  config.params['___k'] = `${config.headers['x-custom-field'] || ''}`;
  return config;
});

Use CDN:

Using jsDelivr CDN:

<script src="https://cdn.jsdelivr.net/npm/xior@0.7.8/dist/xior.umd.js"></script>
<!-- Load plugin -->
<script src="https://cdn.jsdelivr.net/npm/xior@0.7.8/plugins/error-retry.umd.js"></script>

<!-- Usage -->
<script>
  console.log(xior.VERSION);

  xior.plugins.use(xiorErrorRetry());
</script>

Using unpkg CDN:

<script src="https://unpkg.com/xior@0.7.8/dist/xior.umd.js"></script>

<!-- Load plugin -->
<script src="https://unpkg.com/xior@0.7.8/plugins/error-retry.umd.js"></script>

<!-- Usage -->
<script>
  console.log(xior.VERSION);

  xior.plugins.use(xiorErrorRetry());
</script>

Request throttle plugin

Throttle GET requests(or custom) most once per threshold milliseconds, filter repeat requests in certain time.

API:

function throttleRequestPlugin(options: {
  /** threshold in milliseconds, default: 1000ms */
  threshold?: number;
  /**
   * check if we need enable throttle, default only `GET` method or`isGet: true` enable
   */
  enableThrottle?: boolean | ((config?: XiorRequestConfig) => boolean | undefined);
  throttleCache?: ICacheLike<RecordedCache>;
  onThrottle?: (config: XiorRequestConfig) => void;
  throttleItems?: number;
}): XiorPlugin;

The options object:

You can override default value in each request's own config (Except throttleCache)

Param Type Default value Description
threshold number 1000 The number of milliseconds to throttle request invocations to
enableThrottle boolean | ((config: XiorRequestConfig) => boolean | undefined) (config) => config.method === 'GET' || config.isGet Default only enabled in GET request
throttleCache CacheLike lru(100) CacheLike instance that will be used for storing throttled requests, use tiny-lru module
throttleItems number 100 The max number of throttle items in the default LRU cache

Basic usage:

import xior from 'xior';
import throttlePlugin from 'xior/plugins/throttle';

const http = xior.create();
http.plugins.use(
  throttlePlugin({
    onThrottle(config) {
      console.log(`Throttle requests ${config.method} ${config.url}`);
    },
  })
);

http.get('/'); // make real http request
http.get('/'); // response from cache
http.get('/'); // response from cache
http.get('/', { throttle: 2e3 }); // custom throttle to 2 seconds

http.post('/'); // make real http request
http.post('/'); // make real http request
http.post('/'); // make real http request

http.post('/', null, {
  enableThrottle: true,
}); // make real http request
http.post('/', null, {
  enableThrottle: true,
}); // response from cache
http.post('/', null, {
  enableThrottle: true,
}); // response from cache

// make post method as get method use `{isGet: true}`,
// useful when some API is get data but the method is `post`
http.post('/get', null, {
  isGet: true,
}); // make real http request
http.post('/get', null, {
  isGet: true,
}); // response from cache
http.post('/get', null, {
  isGet: true,
}); // response from cache

Use CDN:

Using jsDelivr CDN:

<script src="https://cdn.jsdelivr.net/npm/xior@0.7.8/dist/xior.umd.js"></script>
<!-- Load plugin -->
<script src="https://cdn.jsdelivr.net/npm/xior@0.7.8/plugins/throttle.umd.js"></script>

<!-- Usage -->
<script>
  console.log(xior.VERSION);

  xior.plugins.use(xiorThrottle());
</script>

Using unpkg CDN:

<script src="https://unpkg.com/xior@0.7.8/dist/xior.umd.js"></script>

<!-- Load plugin -->
<script src="https://unpkg.com/xior@0.7.8/plugins/throttle.umd.js"></script>

<!-- Usage -->
<script>
  console.log(xior.VERSION);

  xior.plugins.use(xiorThrottle());
</script>

Request dedupe plugin

Prevents having multiple identical requests on the fly at the same time.

API:

function dedupeRequestPlugin(options: {
  /**
   * check if we need enable dedupe, default only `GET` method or`isGet: true` enable
   */
  enableDedupe?: boolean | ((config?: XiorRequestConfig) => boolean);
  onDedupe?: (config: XiorRequestConfig) => void;
}): XiorPlugin;

Basic usage:

import xior from 'xior';
import dedupePlugin from 'xior/plugins/dedupe';

const http = xior.create();
http.plugins.use(
  dedupePlugin({
    onDedupe(config) {
      console.log(`Dedupe ${config.method} ${config.url}`);
    },
  })
);

http.get('/'); // make real http request
http.get('/'); // response from previous if previous request return response
http.get('/'); // response from previous if previous request return response

http.post('/'); // make real http request
http.post('/'); // make real http request
http.post('/'); // make real http request

Use CDN:

Using jsDelivr CDN:

<script src="https://cdn.jsdelivr.net/npm/xior@0.7.8/dist/xior.umd.js"></script>
<!-- Load plugin -->
<script src="https://cdn.jsdelivr.net/npm/xior@0.7.8/plugins/dedupe.umd.js"></script>

<!-- Usage -->
<script>
  console.log(xior.VERSION);

  xior.plugins.use(xiorDedupe());
</script>

Using unpkg CDN:

<script src="https://unpkg.com/xior@0.7.8/dist/xior.umd.js"></script>

<!-- Load plugin -->
<script src="https://unpkg.com/xior@0.7.8/plugins/dedupe.umd.js"></script>

<!-- Usage -->
<script>
  console.log(xior.VERSION);

  xior.plugins.use(xiorDedupe());
</script>

Error cache plugin

When request error, if have cached data then use the cached data

API:

function errorCachePlugin(options: {
  enableCache?: boolean | ((config?: XiorRequestConfig) => boolean | undefined);
  defaultCache?: ICacheLike<XiorPromise>;
  useCacheFirst?: boolean;
}): XiorPlugin;

The options object:

Param Type Default value Description
enableCache boolean | ((config: XiorRequestConfig) => boolean | undefined) (config) => config.method === 'GET' || config.isGet Default only enabled in GET request
defaultCache CacheLike lru(100, 0) will used for storing requests by default, except you define a custom Cache with your request config, use tiny-lru module
useCacheFirst boolean false If useCacheFirst: true and there's a cache, it will return the cached response first, then run fetching task on the background. This is useful when the response takes a long time, and the data is unnecessary in real-time.
cacheItems number 100 The max number of error cache items in the default LRU cache

Basic usage:

import xior from 'xior';
import errorCachePlugin from 'xior/plugins/error-cache';

const http = xior.create();
http.plugins.use(errorCachePlugin({}));

http.get('/users'); // make real http request, and cache the response
const res = await http.get('/users'); // if request error, use the cache data
if (res.fromCache) {
  // if `fromCache` is true, means data from cache!
  console.log('data from cache!');
  console.log('data cache timestamp: ', res.cacheTime);
  // and get what's the error
  console.log('error', res.error);
}

http.post('/users'); // no cache for post

http.post('/users', { isGet: true }); // but with `isGet: true` can let plugins know this is `GET` behavior! then will cache data

Use CDN:

Using jsDelivr CDN:

<script src="https://cdn.jsdelivr.net/npm/xior@0.7.8/dist/xior.umd.js"></script>
<!-- Load plugin -->
<script src="https://cdn.jsdelivr.net/npm/xior@0.7.8/plugins/error-cache.umd.js"></script>

<!-- Usage -->
<script>
  console.log(xior.VERSION);

  xior.plugins.use(xiorErrorCache());
</script>

Using unpkg CDN:

<script src="https://unpkg.com/xior@0.7.8/dist/xior.umd.js"></script>

<!-- Load plugin -->
<script src="https://unpkg.com/xior@0.7.8/plugins/error-cache.umd.js"></script>

<!-- Usage -->
<script>
  console.log(xior.VERSION);

  xior.plugins.use(xiorErrorCache());
</script>

Cache plugin

Makes xior cacheable

Good to Know: Next.js already support cache for fetch in server side. More detail

Different with error-cache plugin: this plugin will use the data in cache if the cache data not expired.

API:

function cachePlugin(options: {
  enableCache?: boolean | ((config?: XiorRequestConfig) => boolean);
  defaultCache?: ICacheLike<XiorPromise>;
  cacheItems?: number;
}): XiorPlugin;

The options object:

Param Type Default value Description
enableCache boolean | ((config: XiorRequestConfig) => boolean | undefined) (config) => config.method === 'GET' || config.isGet Default only enabled in GET request
defaultCache CacheLike lru(100, 1000*60*5) will used for storing requests by default, except you define a custom Cache with your request config, use tiny-lru module
cacheItems number 100 Custom the default LRU cache numbers
cacheTime number 1000 * 60 * 5 Custom the default LRU cache time

Basic usage:

import xior from 'xior';
import cachePlugin from 'xior/plugins/cache';

const http = xior.create();
http.plugins.use(
  cachePlugin({
    cacheItems: 100,
    cacheTime: 1e3 * 60 * 5,
  })
);

http.get('/users'); // make real http request
http.get('/users'); // get cache from previous request
http.get('/users', { enableCache: false }); // disable cache manually and the real http request

http.post('/users'); // default no cache for post

// enable cache manually in post request
http.post('/users', { enableCache: true }); // make real http request
const res = await http.post('/users', { enableCache: true }); // get cache from previous request
if (res.fromCache) {
  // if `fromCache` is true, means data from cache!
  console.log('data from cache!', res.cacheKey, res.cacheTime);
}

Advanced:

import xior from 'xior';
import cachePlugin from 'xior/plugins/cache';
import { lru } from 'tiny-lru';

const http = xior.create({
  baseURL: 'https://example-domain.com/api',
  headers: { 'Cache-Control': 'no-cache' },
});
http.plugins.use(
  cachePlugin({
    // disable the default cache
    enableCache: false,
    cacheItems: 1000,
    cacheTime: 1e3 * 60 * 10,
  })
);

http.get('/users', { enableCache: true }); // manually enable cache for this request
http.get('/users', { enableCache: true }); // get cache from  previous request

const cacheA = lru(100);
// a actual request made and cached due to force update configured
http.get('/users', { enableCache: true, defaultCache: cacheA, forceUpdate: true });

Persist cache data

How to persist cache data to the filesystem to prevent loss after a server restart?

For more details, refer to this GitHub issue: GitHub issue 33

Upload and download progress plugin

Enable upload and download progress like axios, but the progress is simulated, This means it doesn't represent the actual progress but offers a user experience similar to libraries like axios.

API:

function progressPlugin(options: {
  /** default: 5*1000 ms */
  progressDuration?: number;
}): XiorPlugin;

The options object:

Param Type Default value Description
progressDuration number 5000 The upload or download progress grow to 99% duration

Basic usage:

import xior from 'xior';
import uploadDownloadProgressPlugin from 'xior/plugins/progress';

const http = xior.create({});
http.plugins.use(uploadDownloadProgressPlugin());

const formData = FormData();
formData.append('file', fileObject);
formData.append('field1', 'val1');
formData.append('field2', 'val2');

http.post('/upload', formData, {
  // simulate upload progress to 99% in 10 seconds, default is 5 seconds
  progressDuration: 10 * 1000,
  onUploadProgress(e) {
    console.log(`Upload progress: ${e.progress}%`);
  },
  // onDownloadProgress(e) {
  //   console.log(`Download progress: ${e.progress}%`);
  // },
});

Use CDN:

Using jsDelivr CDN:

<script src="https://cdn.jsdelivr.net/npm/xior@0.7.8/dist/xior.umd.js"></script>
<!-- Load plugin -->
<script src="https://cdn.jsdelivr.net/npm/xior@0.7.8/plugins/progress.umd.js"></script>

<!-- Usage -->
<script>
  console.log(xior.VERSION);

  xior.plugins.use(xiorProgress());
</script>

Using unpkg CDN:

<script src="https://unpkg.com/xior@0.7.8/dist/xior.umd.js"></script>

<!-- Load plugin -->
<script src="https://unpkg.com/xior@0.7.8/plugins/progress.umd.js"></script>

<!-- Usage -->
<script>
  console.log(xior.VERSION);

  xior.plugins.use(xiorProgress());
</script>

Mock plugin

This plugin let you eaisly mock requests

Usage:

with GET:

import xior from 'xior';
import MockPlugin from 'xior/plugins/mock';

const instance = xior.create();
const mock = new MockPlugin(instance);

// Mock any GET request to /users
// arguments for reply are (status, data, headers)
mock.onGet('/users').reply(
  200,
  {
    users: [{ id: 1, name: 'John Smith' }],
  },
  {
    'X-Custom-Response-Header': '123',
  }
);

instance.get('/users').then(function (response) {
  console.log(response.data);
  console.log(response.headers.get('X-Custom-Response-Header')); // 123
});

// Mock GET request to /users when param `searchText` is 'John'
// arguments for reply are (status, data, headers)
mock.onGet('/users', { params: { searchText: 'John' } }).reply(200, {
  users: [{ id: 1, name: 'John Smith' }],
});

instance.get('/users', { params: { searchText: 'John' } }).then(function (response) {
  console.log(response.data);
});

with POST:

import xior from 'xior';
import MockPlugin from 'xior/plugins/mock';

const instance = xior.create();
const mock = new MockPlugin(instance);

// Mock any POST request to /users
// arguments for reply are (status, data, headers)
mock.onPost('/users').reply(
  200,
  {
    users: [{ id: 1, name: 'John Smith' }],
  },
  {
    'X-Custom-Response-Header': '123',
  }
);

instance.post('/users').then(function (response) {
  console.log(response.data);
  console.log(response.headers.get('X-Custom-Response-Header')); // 123
});

// Mock POST request to /users when param `searchText` is 'John'
// arguments for reply are (status, data, headers)
mock.onPost('/users', null, { params: { searchText: 'John' } }).reply(200, {
  users: [{ id: 1, name: 'John Smith' }],
});

instance.get('/users', null, { params: { searchText: 'John' } }).then(function (response) {
  console.log(response.data);
});

// Mock POST request to /users when body `searchText` is 'John'
// arguments for reply are (status, data, headers)
mock.onPost('/users', { searchText: 'John' }).reply(200, {
  users: [{ id: 1, name: 'John Smith' }],
});

instance.get('/users', { searchText: 'John' }).then(function (response) {
  console.log(response.data);
});

More details, check here.

Use CDN:

Using jsDelivr CDN:

<script src="https://cdn.jsdelivr.net/npm/xior@0.7.8/dist/xior.umd.js"></script>
<!-- Load plugin -->
<script src="https://cdn.jsdelivr.net/npm/xior@0.7.8/plugins/mock.umd.js"></script>

<!-- Usage -->
<script>
  console.log(xior.VERSION);

  const mock = new xiorMock(xior);
</script>

Using unpkg CDN:

<script src="https://unpkg.com/xior@0.7.8/dist/xior.umd.js"></script>

<!-- Load plugin -->
<script src="https://unpkg.com/xior@0.7.8/plugins/mock.umd.js"></script>

<!-- Usage -->
<script>
  console.log(xior.VERSION);

  const mock = new xiorMock(xior);
</script>

Auth refresh token plugin(from community)

We will use xior-auth-refresh plugin from the community: https://github.com/Audiu/xior-auth-refresh

Install:

npm install xior-auth-refresh --save
# or
yarn add xior-auth-refresh
# or
pnpm add xior-auth-refresh

Usage:

import xior from 'xior';
import createAuthRefreshInterceptor from 'xior-auth-refresh';

// Function that will be called to refresh authorization
const refreshAuthLogic = (failedRequest) =>
  xior.post('https://www.example.com/auth/token/refresh').then((tokenRefreshResponse) => {
    localStorage.setItem('token', tokenRefreshResponse.data.token);
    failedRequest.response.config.headers['Authorization'] =
      'Bearer ' + tokenRefreshResponse.data.token;
    return Promise.resolve();
  });

// Instantiate the interceptor
createAuthRefreshInterceptor(xior, refreshAuthLogic);

// Make a call. If it returns a 401 error, the refreshAuthLogic will be run,
// and the request retried with the new token
xior.get('https://www.example.com/restricted/area').then(/* ... */).catch(/* ... */);

More: https://github.com/Audiu/xior-auth-refresh

Auth refresh token plugin(built-in)

Usage:

import xior, { XiorResponse } from 'xior';
import errorRetry from 'xior/plugins/error-retry';
import setupTokenRefresh from 'xior/plugins/token-refresh';

const instance = xior.create();

const TOKEN_KEY = 'TOKEN';
function getToken() {
  return localStorage.getItem(TOKEN_KEY);
}
function setToken(token: string) {
  return localStorage.setItem(TOKEN_KEY, token);
}
function deleteToken() {
  return localStorage.getItem(TOKEN_KEY);
}

instance.interceptors.request.use((config) => {
  const token = getToken();
  if (token) {
    config.headers['Authorization'] = `Bearer ${token}`;
  }
  return config;
});

function shouldRefresh(response: XiorResponse) {
  const token = getToken();
  return Boolean(token && response?.status && [401, 403].includes(response.status));
}
instance.plugins.use(
  errorRetry({
    enableRetry: (config, error) => {
      if (error?.response && shouldRefresh(error.response)) {
        return true;
      }
      // return false
    },
  })
);
setupTokenRefresh(http, {
  shouldRefresh,
  async refreshToken(error) {
    try {
      const { data } = await http.post('/token/new');
      if (data.token) {
        setToken(data.token);
      } else {
        throw error;
      }
    } catch (e) {
      // something wrong, delete old token
      deleteToken();
      return Promise.reject(error);
    }
  },
});

Create your own custom plugin

xior let you easily to create custom plugins.

Here are examples:

  1. Simple Logging plugin:
import xior from 'xior';

const instance = xior.create();
instance.plugins.use(function logPlugin(adapter, instance) {
  return async (config) => {
    const start = Date.now();
    const res = await adapter(config);
    console.log('%s %s %s take %sms', config.method, config.url, res.status, Date.now() - start);
    return res;
  };
});
  1. Check built-in plugins get more inspiration:

Check src/plugins

Cleanup plugins example

import xior from 'xior';
import errorRetryPlugin from 'xior/plugins/error-retry';
const http = xior.create();

const pluginHandler = http.plugins.use(errorRetryPlugin());
http.plugins.eject(pluginHandler);

// Cleanup all plugins
// http.plugins.clear()

Helper functions

xior has built-in helper functions, may useful for you:

import lru from 'tiny-lru';
import {
  encodeParams,
  merge as deepMerge,
  delay as sleep,
  buildSortedURL,
  isAbsoluteURL,
  joinPath,
  isXiorError,
  trimUndefined,
  Xior,
} from 'xior';

FAQ

xior frequently asked questions.

1. Is xior 100% compatiable with axios?

No, but xior offers a similar API like axios: axios.create / axios.interceptors / .get/post/put/patch/delete/head/options.

The most common change is replacing axios with xior and checking if the TypeScript types pass:

import axios, {
  XiorError as AxiosError,
  isXiorError as isAxiosError,
  XiorRequestConfig as AxiosRequestConfig,
  XiorResponse as AxiosResponse,
} from 'xior';

const instance = axios.create({
  baseURL: '...',
  timeout: 20e3,
});

2. Can I use xior in projects like Bun, Expo, React Native, RemixJS, Next.js, Vue, Nuxt.js, Tauri or NervJS/Taro?

Yes, xior works anywhere where the native fetch API is supported. Even if the environment doesn't support fetch, you can use a fetch polyfill like for older browsers.

For Tauri or Taro: check 3. How can I use custom fetch implementation or How to support proxy feature?

3. How can I use custom fetch implementation or How to support proxy feature?

To support proxy feature or custom fetch implementation, we can use node-fetch, nodejs undici, or @tauri-apps/plugin-http module's fetch implementation to replace the built-in fetch.

For example undici:

npm install undici
import { fetch as undiciFetch, FormData, Agent, type RequestInit as RequestInit_ } from 'undici';

/** For TypeScript types **/
declare global {
  interface RequestInit extends RequestInit_ {}
}

/** Create Agent **/
const agent = new Agent({
  connections: 10,
});

const xiorInstance = xior.create({
  baseURL: 'https://example.com',
  fetch: undiciFetch,
  dispatcher: agent,
});

For example node-fetch:

# For ESM module
npm install node-fetch

# For CommonJS module
# npm install node-fetch@v2.7.0
# npm install @types/node-fetch -D
import nodeFetch, { RequestInit as RequestInit_ } from 'node-fetch';
import http from 'node:http';
import https from 'node:https';

/** For TypeScript types **/
declare global {
  interface RequestInit extends RequestInit_ {}
}

/** Create Agent **/
const httpAgent = new http.Agent({
  keepAlive: true,
});
const httpsAgent = new https.Agent({
  keepAlive: true,
});

const xiorInstance = xior.create({
  baseURL: 'https://example.com',
  fetch: nodeFetch,
  // agent: httpAgent,
  agent(_parsedURL) {
    if (_parsedURL.protocol === 'http:') {
      return httpAgent;
    } else {
      return httpsAgent;
    }
  },
});

Use @tauri-apps/plugin-http's fetch implementaion in Tauri:

import { fetch } from '@tauri-apps/plugin-http';
import xior from 'xior';

export const http = xior.create({
  baseURL: 'https://www.tauri.app',
  fetch,
});

async function test() {
  const { data } = await http.get('/');
  return data;
}

For Taro:

import { fetch } from 'taro-fetch-polyfill';
import xior from 'xior';

// fetch('https://api.github.com')
//     .then(response => response.json())
//     .then(console.log);

export const http = xior.create({
  baseURL: 'https://github.com/NervJS/taro',
  fetch,
});

async function test() {
  const { data } = await http.get('/');
  return data;
}

4. How do I handle responses with types like 'stream', 'document', 'arraybuffer', or 'blob'?

When {responseType: 'blob'| 'arraybuffer'}:

xior.get('https://exmaple.com/some/api', { responseType: 'blob' }).then((response) => {
  console.log(response.data); // response.data is a Blob
});

// Same with
fetch('https://exmaple.com/some/api')
  .then((response) => response.blob())
  .then((data) => {
    console.log(data); // is a Blob
  });
xior.get('https://exmaple.com/some/api', { responseType: 'arraybuffer' }).then((response) => {
  console.log(response.data); // response.data is a ArrayBuffer
});

// Same with
fetch('https://exmaple.com/some/api')
  .then((response) => response.arraybuffer())
  .then((data) => {
    console.log(data); // is a ArrayBuffer
  });

But when responseType set to 'stream', 'document', 'custom' or 'original', Xior will return the original fetch response and res.data will be undefined:

fetch('https://exmaple.com/some/api').then((response) => {
  console.log(response);
});

// same with
xior.get('https://exmaple.com/some/api', { responseType: 'stream' }).then((res) => {
  console.log(res.response); // But res.data will be undefined
});

And to handle a stream response, use the responseType: 'stream' option in your request, then do something with the response as fetch does:

import xior from 'xior';

const http = xior.create({ baseURL });

const { response } = await http.post<{ file: any; body: Record<string, string> }>(
  '/stream/10',
  null,
  { responseType: 'stream' }
);
const reader = response.body!.getReader();
let chunk;
for await (chunk of readChunks(reader)) {
  console.log(`received chunk of size ${chunk.length}`);
}

5. How do I support older browsers?

You can use a polyfill for the fetch API. Check the file src/tests/polyfill.test.ts for a potential example.

6. Why is xior named "xior"?

The original name axior was unavailable on npm, so when removed the "a": axior.

7. Where can I ask additional questions?

If you have any questions, feel free to create issues.

Migrate from axios to xior

The most common change is replacing axios with xior and checking if the TypeScript types pass:

import axios, {
  XiorError as AxiosError,
  isXiorError as isAxiosError,
  XiorRequestConfig as AxiosRequestConfig,
  XiorResponse as AxiosResponse,
} from 'xior';

const instance = axios.create({
  baseURL: '...',
  timeout: 20e3,
});

GET

axios:

import axios from 'axios';

// Make a request for a user with a given ID
axios.get('/user?ID=12345');

// Optionally the request above could also be done as
axios.get('/user', {
  params: {
    ID: 12345,
  },
});

// Want to use async/await? Add the `async` keyword to your outer function/method.
async function getUser() {
  try {
    const response = await axios.get('/user?ID=12345');
    console.log(response);
  } catch (error) {
    console.error(error);
  }
}

xior:

import axios from 'xior';

// Make a request for a user with a given ID
axios.get('/user?ID=12345');

// Optionally the request above could also be done as
axios.get('/user', {
  params: {
    ID: 12345,
  },
});

// Want to use async/await? Add the `async` keyword to your outer function/method.
async function getUser() {
  try {
    const response = await axios.get('/user?ID=12345');
    console.log(response);
  } catch (error) {
    console.error(error);
  }
}

POST

axios:

import axios from 'axios';

axios.post('/user', {
  firstName: 'Fred',
  lastName: 'Flintstone',
});

xior:

import axios from 'xior';

axios.post('/user', {
  firstName: 'Fred',
  lastName: 'Flintstone',
});

axios(requestObj): axios({ method: 'get', params: { a: 1 } })

axios:

import axios from 'axios';

await axios({ method: 'get', params: { a: 1 } });

xior:

import xior from 'xior';

const axios = xior.create();

await axios.request({ method: 'get', params: { a: 1 } });

Creating an instance

axios:

import axios from 'axios';
const instance = axios.create({
  baseURL: 'https://some-domain.com/api/',
  timeout: 1000,
  headers: { 'X-Custom-Header': 'foobar' },
});

xior:

import axios from 'xior';
const instance = axios.create({
  baseURL: 'https://some-domain.com/api/',
  timeout: 1000,
  headers: { 'X-Custom-Header': 'foobar' },
});

Get response headers

axios:

import axios from 'axios';

const axiosInstance = axios.create({
  baseURL: 'https://apiexampledomian.com/api',
});

const { data, headers } = await axiosInstance.get('/');

console.log(headers['X-Header-Name']);

xior:

import xior from 'xior';

const xiorInstance = xior.create({
  baseURL: 'https://apiexampledomian.com/api',
});

const { data, headers } = await xiorInstance.get('/');

console.log(headers.get('X-Header-Name'));

Download file with responseType: 'stream' | 'blob'

axios:

import axios from 'axios';
import fs from 'fs';

// GET request for remote image in Node.js
axios({
  method: 'get',
  url: 'https://bit.ly/2mTM3nY',
  responseType: 'stream',
}).then(function (response) {
  response.data.pipe(fs.createWriteStream('ada_lovelace.jpg'));
});

// For browser
axios({
  method: 'get',
  url: 'https://bit.ly/2mTM3nY',
  responseType: 'blob',
}).then(function (response) {
  // create file link in browser's memory
  const href = URL.createObjectURL(response.data);

  // create "a" HTML element with href to file & click
  const link = document.createElement('a');
  link.href = href;
  link.setAttribute('download', 'file.pdf'); //or any other extension
  document.body.appendChild(link);
  link.click();

  // clean up "a" element & remove ObjectURL
  document.body.removeChild(link);
  URL.revokeObjectURL(href);
});

xior:

// Node.js
import xior from 'xior';
const axios = xior.create();

axios
  .get('https://bit.ly/2mTM3nY', {
    responseType: 'stream',
  })
  .then(async function ({ response, config }) {
    const buffer = Buffer.from(await response.arrayBuffer());
    return writeFile('ada_lovelace.jpg', buffer);
  });

// For browser
xior
  .get('https://d2l.ai/d2l-en.pdf', {
    headers: {
      Accept: 'application/pdf',
    },
    responseType: 'blob',
  })
  .then((res) => {
    const { data: blob } = res;
    var url = window.URL.createObjectURL(blob);
    var a = document.createElement('a');
    a.href = url;
    a.download = 'filename.pdf';
    document.body.appendChild(a); // we need to append the element to the dom -> otherwise it will not work in firefox
    a.click();
    a.remove(); //afterwards we remove the element again
  });

Use stream

axios:

import axios from 'axios';
import { Readable } from 'stream';

const http = axios.create();

async function getStream(url: string, params: Record<string, any>) {
  const { data } = await http.get(url, {
    params,
    responseType: 'stream',
  });
  return data;
}

xior:

import axxios from 'xior';
import { Readable } from 'stream';

const http = axios.create();

async function getStream(url: string, params: Record<string, any>) {
  const { response } = await http.get(url, {
    params,
    responseType: 'stream',
  });
  const stream = convertResponseToReadable(response);
  return stream;
}

function convertResponseToReadable(response: Response): Readable {
  const reader = response.body.getReader();
  return new Readable({
    async read() {
      const { done, value } = await reader.read();
      if (done) {
        this.push(null);
      } else {
        this.push(Buffer.from(value));
      }
    },
  });
}

Migrate from fetch to xior

GET

fetch:

async function logMovies() {
  const response = await fetch('http://example.com/movies.json?page=1&perPage=10');
  const movies = await response.json();
  console.log(movies);
}

xior:

import xior from 'xior';

const http = xior.create({
  baseURL: 'http://example.com',
});
async function logMovies() {
  const { data: movies } = await http.get('/movies.json', {
    params: {
      page: 1,
      perPage: 10,
    },
  });
  console.log(movies);
}

POST

fetch:

// Example POST method implementation:
async function postData(url = '', data = {}) {
  // Default options are marked with *
  const response = await fetch(url, {
    method: 'POST', // *GET, POST, PUT, DELETE, etc.
    mode: 'cors', // no-cors, *cors, same-origin
    cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
    credentials: 'same-origin', // include, *same-origin, omit
    headers: {
      // 'Content-Type': 'application/json',
      // 'Content-Type': 'application/x-www-form-urlencoded',
    },
    redirect: 'follow', // manual, *follow, error
    referrerPolicy: 'no-referrer', // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
    body: JSON.stringify(data), // body data type must match "Content-Type" header
  });
  return response.json(); // parses JSON response into native JavaScript objects
}

postData('https://example.com/answer', { answer: 42 }).then((data) => {
  console.log(data); // JSON data parsed by `data.json()` call
});

xior:

import xior from 'xior';

const http = xior.create({
  baseURL: 'http://example.com',
});

http
  .post(
    '/answer',
    { answer: 42 },
    {
      mode: 'cors', // no-cors, *cors, same-origin
      cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
      credentials: 'same-origin', // include, *same-origin, omit
      headers: {
        // 'Content-Type': 'application/json',
        // 'Content-Type': 'application/x-www-form-urlencoded',
      },
      redirect: 'follow', // manual, *follow, error
      referrerPolicy: 'no-referrer', // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
    }
  )
  .then(({ data }) => {
    console.log(data);
  });

Abort a fetch

fetch:

const controller = new AbortController();
const signal = controller.signal;
const url = 'video.mp4';

const downloadBtn = document.querySelector('#download');
const abortBtn = document.querySelector('#abort');

downloadBtn.addEventListener('click', async () => {
  try {
    const response = await fetch(url, { signal });
    console.log('Download complete', response);
  } catch (error) {
    console.error(`Download error: ${error.message}`);
  }
});

abortBtn.addEventListener('click', () => {
  controller.abort();
  console.log('Download aborted');
});

xior:

import xior from 'xior';

const http = xior.create();

const controller = new AbortController();
const signal = controller.signal;
const url = 'video.mp4';

const downloadBtn = document.querySelector('#download');
const abortBtn = document.querySelector('#abort');

downloadBtn.addEventListener('click', async () => {
  try {
    const response = await http.get(url, { signal });
    console.log('Download complete', response);
  } catch (error) {
    console.error(`Download error: ${error.message}`);
  }
});

abortBtn.addEventListener('click', () => {
  controller.abort();
  console.log('Download aborted');
});

Sending a request with credentials included

fetch:

fetch('https://example.com', {
  credentials: 'include',
});

xior:

import xior from 'xior';

const http = xior.create();

http.get('https://example.com', {
  credentials: 'include',
});

Uploading a file

fetch:

async function upload(formData) {
  try {
    const response = await fetch('https://example.com/profile/avatar', {
      method: 'PUT',
      body: formData,
    });
    const result = await response.json();
    console.log('Success:', result);
  } catch (error) {
    console.error('Error:', error);
  }
}

const formData = new FormData();
const fileField = document.querySelector('input[type="file"]');

formData.append('username', 'abc123');
formData.append('avatar', fileField.files[0]);

upload(formData);

xior:

import xior from 'xior';

const http = xior.create({
  baseURL: 'https://example.com',
});

async function upload(formData) {
  try {
    const { data: result } = await http.put('/profile/avatar', formData);
    console.log('Success:', result);
  } catch (error) {
    console.error('Error:', error);
  }
}

const formData = new FormData();
const fileField = document.querySelector('input[type="file"]');

formData.append('username', 'abc123');
formData.append('avatar', fileField.files[0]);

upload(formData);

Processing a text file line by line

fetch:

async function* makeTextFileLineIterator(fileURL) {
  const utf8Decoder = new TextDecoder('utf-8');
  const response = await fetch(fileURL);
  const reader = response.body.getReader();
  let { value: chunk, done: readerDone } = await reader.read();
  chunk = chunk ? utf8Decoder.decode(chunk) : '';

  const newline = /\r?\n/gm;
  let startIndex = 0;
  let result;

  while (true) {
    const result = newline.exec(chunk);
    if (!result) {
      if (readerDone) break;
      const remainder = chunk.substr(startIndex);
      ({ value: chunk, done: readerDone } = await reader.read());
      chunk = remainder + (chunk ? utf8Decoder.decode(chunk) : '');
      startIndex = newline.lastIndex = 0;
      continue;
    }
    yield chunk.substring(startIndex, result.index);
    startIndex = newline.lastIndex;
  }

  if (startIndex < chunk.length) {
    // Last line didn't end in a newline char
    yield chunk.substr(startIndex);
  }
}

async function run() {
  for await (const line of makeTextFileLineIterator(urlOfFile)) {
    processLine(line);
  }
}

run();

xior:

Good to Know: add {responseType: 'stream'} options will tell xior no need process response, and return original response in format {response}

import xior from 'xior';

const http = xior.create();

async function* makeTextFileLineIterator(fileURL) {
  const utf8Decoder = new TextDecoder('utf-8');
  const { response } = await http.get(fileURL, { responseType: 'stream' });
  const reader = response.body.getReader();
  let { value: chunk, done: readerDone } = await reader.read();
  chunk = chunk ? utf8Decoder.decode(chunk) : '';

  const newline = /\r?\n/gm;
  let startIndex = 0;
  let result;

  while (true) {
    const result = newline.exec(chunk);
    if (!result) {
      if (readerDone) break;
      const remainder = chunk.substr(startIndex);
      ({ value: chunk, done: readerDone } = await reader.read());
      chunk = remainder + (chunk ? utf8Decoder.decode(chunk) : '');
      startIndex = newline.lastIndex = 0;
      continue;
    }
    yield chunk.substring(startIndex, result.index);
    startIndex = newline.lastIndex;
  }

  if (startIndex < chunk.length) {
    // Last line didn't end in a newline char
    yield chunk.substr(startIndex);
  }
}

async function run() {
  for await (const line of makeTextFileLineIterator(urlOfFile)) {
    processLine(line);
  }
}

run();

API Reference

Star History

Star History Chart

Thanks

Without the support of these resources, xior wouldn't be possible:

changelog

CHANGELOG 📝

v0.7.8

v0.7.7

  • Feat: Support application/vnd.api+json and application/x-www-form-urlencoded; charset=utf-8 in Request Content-Type
  • Chore: Remove export { type A }'s type in *.d.ts after build (some old TypeScript versions (before 3.8) don't support this syntax)
  • Chore: Improve README and make minor code improvements

v0.7.6

  • Fix: when params is undefined, throw TypeError [Error]: Cannot convert undefined or null to object

v0.7.5

  • Fix(build): Change target from exnext to es2015, because in some boilerplates(expo, vue-cli), will throw error **Static class blocks are not enabled. Please add@babel/plugin-transform-class-static-blockto your configuration.

v0.7.4

  • Fix(plugins): For the errorRetry plugin, if enableRetry is a function returning undefined or is undefined, it only retries GET requests and skips POST requests; same with other plugins adjustments.
  • Chore: Add https://www.jsdocs.io/package/xior to README's API Reference section
  • Chore(build): switc to tsup bundler from bunchee, reduce the output size from 7KB(GZIP 3KB) to 5KB(GZIP 2.5KB)!!

v0.7.3

  • Refactor: minor improve size
  • Plugins: use relative path to import utils and types

v0.7

  • Refactor: Don't exports unnecessary internal properties
  • Chore: update README.md

v0.7.1

  • Chore: remove ts-deepmerge from dependencies
  • Docs: update README to fix about node-fetch agent example code
  • Refactor: minor reduce size

v0.7.0

  • Feat: Add fetch option; now we can use node-fetch and undici as replacements for the built-in fetch to support the proxy feature, or you can easily use other fetch implementations. How can I use custom fetch implementation or How to support proxy feature?
  • Tests: Add tests for node-fetch and undici replacements.
  • Refactor: Minor improvement.
  • Build: Add sourcemap output by default with updated bunchee version.
  • README: Update README about fetch option.
  • Good to Know: Change internal properties ._RSIRun to _did and .fetch to ._.

v0.6.3

  • Refactor: Add cacheKey to cache plugin for persistent cache
  • Tests: Add persistent cache test

v0.6.2

v0.6.1

  • Fix: response interceptors should run only once with error-retry plugin

v0.6.0

v0.5.5

  • Refactor: content-type detecting code improve and decrease size

v0.5.4

  • feat(core): Support URLSearchParams Fix issues/26

v0.5.3

  • fix(core): RangeError: Invalid time value. ref issues/23

v0.5.2 2024/7/9

  • Fix(core): if params include Date value, call .toISOString() and utils encodeParams support options allowDot: true and arrayFormat: 'indices' | 'repeat' | 'brackets' (default is 'indices'). Fix issues/22 and issues/23

Code example:

import xior, { encodeParams } from 'xior';

const filter = {
  ids: [1, 2, 3],
  dateFrom: new Date(),
  dateTo: new Date(),
};

const http = xior.create({
  paramsSerializer: (params: any) =>
    encodeParams(params, true, null, {
      allowDots: false,
      arrayFormat: 'indices', // 'indices' | 'repeat' | 'brackets'
      serializeDate: (date) => date.toISOString(),
    }),
});

/* 
'indices': { a: ['b', 'c'] } -> 'a[0]=b&a[1]=c'
'brackets': { a: ['b', 'c'] } -> 'a[]=b&a[]=c'
'repeat': { a: ['b', 'c'] } -> 'a=b&a=c'
*/

http.get('https://example.com', { params: { filter } });

v0.5.1 2024/5/28

  • Feat(core): if request config withCredentials: true, before useless, now will set fetch config credentials: 'include'. Fix issues/19

v0.5.0 2024/5/19

This is a breaking change:

  • Feat(core): The xior class should be CapitalCase like: Xior. Fix issues/18

Migration

If you always use import xior from 'xior';, you can ignore migration code below.

Before:

import { xior } from 'xior';

Now:

import { Xior } from 'xior';

v0.4.2 2024/5/2

  • Feat(new plugin): add token refresh plugin

v0.4.1 2024/04/29

  • Feat: remove undefined value in params / data

v0.4.0 2024/04/24

Breaking Change

This version is about Axios compatible issue in some cases. Fixing https://github.com/suhaotian/xior/issues/12 and https://github.com/suhaotian/xior/issues/15.

  • Feat(core): when responseType: 'blob' | 'arraybuffer' then the response.data is Blob and ArrayBuffer, no need response.blob() or response.arraybuffer() anymore.
  • Fix(interceptors): make sure the multiple response interceptors chain behavior same as axios's interceptors.

v0.3.13 2024/04/21

  • Feat(plugin): add custom parameters of LRU in plugins: cache, error-cache, throttle
  • Feat(plugin): add cacheTime to cache plugin

v0.3.12 2024/04/13

  • fix(plugin): fix error cache plugin cacheTime is undefined when useCacheFirst: true

v0.3.11 2024/04/12

  • feat(plugin): error-cache plugin add cacheTime to the response

v0.3.10 2024/04/11

  • feat(plugin): error-retry plugin's retryInterval add config and error to parameters

v0.3.9 2024/04/9

  • feat(core): add try catch to await fetch(...)

Now you can capture the request error in response interceptors, and the error will be TypeError:

import xior, { merge } from 'xior';

const http = xior.create({
  // ...options
});

http.interceptors.response.use(
  (result) => {
    return result;
  },
  async (error) => {
    if (error instanceof TypeError) {
      console.log(`Request error:`, error);
    }
    if (error?.response?.status === 401) {
      localStorage.removeItem('REQUEST_TOKEN');
    }
  }
);

v0.3.8 2024/04/8

  • feat(plugins): enhance plugins's enable* logic

Now you can return undefined in enable* method:

import xior from 'xior';
import errorRetryPlugin from 'xior/plugins/error-retry';

const http = xior.create();
http.plugins.use(
  errorRetryPlugin({
    enableRetry(config, error) {
      if (error.response?.status === 401) {
        return false;
      }
      // no return here, and will reuse the default `enableRetry` logic
    },
  })
);

v0.3.7 2024/04/7

  • feat(plugin): add useCacheFirst to error cache plugin

If useCacheFirst: true and there's a cache, it will return the cached response first, then run fetching in the background. This is useful when the response takes a long time, and the data is unnecessary in real-time.

v0.3.6 2024/04/6

  • feat(plugin): add onThrottle to throttle plugin for logging purpose

v0.3.5 2024/03/30

  • feat(plugin): add onDedupe to dedupe plugin for logging purpose

v0.3.2 2024/03/30

  • feat: reduce build size use common xior/utils
  • chore: bump bunchee to v5.0.1

v0.3.1 2024/03/25

  • fix(error-retry plugin): if have request interceptors, when error retry, retry with the latest request config from request interceptors

v0.3.0 2024/03/24

  • fix(core): POST/DELETE/PUT/PATCH methods when content-type=application/x-www-form-urlencoded, use formData to in body (previous put in url)
  • refactor(core): default request interceptors should work before send fetch
  • refactor(core): remove _data in request config
  • refactor(core): remove encode in options, use paramsSerializer option instead
  • chore(README): add encrypt/decrypt example to README

v0.2.6 2024/03/22

  • fix(core): when post with headers: { 'content-type': 'application/x-www-form-urlencoded' }, shouldn't post with body
  • chore(core): shorter naming words

v0.2.5 2024/03/20

  • fix(plugin): fix error-retry plugin default options override bugs
  • fix(plugin): requestConfig with plugins should always get latest config from request interceptors

v0.2.4

  • fix(plugin): fix mock plugin not working after bundle
  • chore(tests): refactor tests to use bundled files to run tests

v0.2.3 2024/03/19

  • fix(plugin): fix error-retry plugin not working after bundle

v0.2.2

  • fix(plugin): fix error-retry plugin, TypeError should retry too
  • feat(plugin): error-retry plugin, retryInterval can be function too, and add onRetry to options
  • chore(core): minor improvement

v0.2.1 2024/03/17

  • feat(core): support direct call xior.get/post.. similar to axios.get/post.. API, no need create instance at first
  • fix(core): DELETE and OPTIONS method's data option should be url encoded format like GET / HEAD
  • feat: add UMD (Universal Module Definition) format bundle (now you can directly load xior in browser)
  • feat: add VERSION to xior, now you can get current version of xior by: import xior from 'xior'; console.log(xior.VERSION)
  • feat(new plugin): add error-cache plugin
  • feat(new plugin): add dedupe plugin
  • feat(new plugin): add mock plugin

Breaking Change:

  1. Type

before:

import xior from 'xior';

let instance: xior;
instance = xior.create();

Now:

import xior, { XiorInstance } from 'xior';

let instance: XiorInstance;
instance = xior.create();
  1. OPTIONS method

before:

import xior, { XiorInstance } from 'xior';

const instance = xior.create();
instance.options('/options_api_path', { text: 'this is data' }, { params: { a: 1 } });

now:

import xior, { XiorInstance } from 'xior';

const instance = xior.create();
instance.options('/options_api_path', { params: { a: 1, text: 'this is data' } });

v0.1.4 2024-03-09

  • Feat(core): support xiorInstance.defaults.headers['Authorization'] = 'Basic token';

v0.1.3 2024-03-08

  • Feat(core): add isGet?: boolean option

v0.1.2

  • Feat(cache plugin): add fromCache: boolean in cache plugin

v0.1.1 2024-03-04

  • Fix: compatible delete method with axios, and delete method shouldn't have body
  • Chore: remove unused code in core module

Breaking change:

import xior from 'xior';

const http = xior.create({ baseURL: 'https://exampled.com' });

// before
http.delete('/', {}, { params: { a: 1, b: 2 } });

// now
http.delete('/', { params: { a: 1, b: 2 } });

v0.0.10 2024-03-01

  • chore(build): Update build config to ensure consistency of plugin import paths
  • chore(doc): Update README

v0.0.9 2024-02-29

  • fix(plugins): resolve import plugins not found file error in expo (react-native) project

v0.0.8 2024-02-29

  • feat(core): compatible axios's options: paramsSerializer and withCredentials

v0.0.7 2024-02-27

  • feat(core): support nested object parameters in default
  • feat(plugin): implemented error retry, cache, throttle, and upload/download progress plugins
  • fix(build): resolved Bunchee build output error with Vite projects.
  • chore(doc): updated README.md
  • chore(examples): add bun, vite, and next build example for make sure it's working in these projects

v0.0.6 2024-02-24

  • feat(plugin): Plugin mechanism implemented 🖖
  • feat: Compatibility with polyfills in older environments

v0.0.5 2024-02-20

  • fix: resolved issues with GitHub Actions release

v0.0.4

  • feat: support url as first parameter in xiorInstance.request('/url')
  • feat: Removed first parameter url from xiorInstance.request

v0.0.3

  • Chore: Enhanced README and added more tests
  • Feat: xiorInstance.request remove first parameter url

v0.0.2 2024-02-18

  • Feat: improved error handling compatibility with Axios's response interceptor.

v0.0.1 2024-02-15

🐣