Skip to content

Repository files navigation

KnHttpJS

Lightweight HTTP client for the browser (based on XMLHttpRequest), written in TypeScript.

  • async / await and chainable callbacks (onSuccess, onError, onEnd) on the same request object
  • get, post, put, patch, delete with json, form, formData (with File / Blob) or raw bodies
  • Instances with their own options (baseUrl, auth, headers...), hooks and automatic retries
  • Query string parameters, abort with AbortSignal, upload and download progress
  • Auth token with a configurable header and scheme, HTTP basic auth, CSRF token
  • Strongly typed: generic response data, typed bodies and options, optional runtime validation
  • ES module and browser script (IIFE) builds, no dependency, compatible with the reactive states (Vue, Pinia...)

Table of contents

Installation

With a package manager (ES module)

pnpm add kn-http
npm install kn-http

The TypeScript declarations are included in the package, nothing else to install.

Browser script (IIFE)

Load the script from the dist folder, from node_modules/kn-http/dist/ or from a CDN:

<script src="https://cdn.jsdelivr.net/npm/kn-http@4/dist/kn-http.iife.min.js"></script>

The script defines the global KnHttp object.

Dist files

File Format Usage
dist/kn-http.js ES module import KnHttp from 'kn-http' (Vite, webpack, Rollup...)
dist/kn-http.iife.js Browser script <script>, defines the global KnHttp
dist/kn-http.iife.min.js Browser script (minified) <script>, defines the global KnHttp
dist/types/ TypeScript declarations Used automatically by TypeScript and the editors

Quick start

TypeScript (npm)

import KnHttp from 'kn-http';

interface User {
	id: number;
	name: string;
}

// API instance with its own options
const api = KnHttp.create({
	baseUrl: '/api',
	authToken: () => localStorage.getItem('token'),
	retry: 2
});

async function loadUsers(): Promise<User[]> {
	// The generic type is the type of res.data (unknown by default)
	const res = await api.get<User[]>('/users', { params: { page: 1 } });
	return res.data;
}

async function saveUser(user: User): Promise<void> {
	saveBtn.disabled = true;

	try {
		await api.put(`/users/${user.id}`, { json: user });
		notifySuccess('User saved');
	} catch (err) {
		if (KnHttp.isCanceled(err)) return;
		if (KnHttp.isKnHttpError(err) && err.code == KnHttp.HTTP_ERROR) notifyError('Error ' + err.status);
		else notifyError('Please check your internet connection');
	} finally {
		saveBtn.disabled = false;
	}
}

JavaScript (npm, ES module)

import KnHttp from 'kn-http';

const api = KnHttp.create({ baseUrl: '/api', csrf: csrfToken });

async function saveSettings(settings) {
	try {
		const res = await api.post('/account/settings', { form: settings });
		if (res.data.success) notifySuccess('Settings saved');
	} catch (err) {
		if (KnHttp.isCanceled(err)) return;
		notifyError(err.code == KnHttp.NETWORK_ERROR ? 'Please check your internet connection' : err.message);
	}
}

Editors like VS Code also use the TypeScript declarations in JavaScript files (autocompletion and documentation).

JavaScript (browser script, IIFE)

The callbacks work in any function (no async needed), await can be used too:

<script src="kn-http.iife.min.js"></script>
<script>
	KnHttp.defaults.csrf = csrfToken;

	// Callbacks
	function saveSettings() {
		KnHttp.post('/account/settings', { form: { theme: 'dark' } })
			.onSuccess(res => console.log('Saved', res.data))
			.onError(err => {
				if (err.code == KnHttp.CANCELED_ERROR) return;
				console.error(err.message, err.status, err.data);
			})
			.onEnd(wasSuccess => console.log('Request ended', wasSuccess));
	}

	// async / await
	async function refreshStats() {
		const res = await KnHttp.get('/api/stats');
		console.log(res.data);
	}
</script>

Usage

Callbacks

Each request method returns a request object with chainable callbacks, usable in any function:

const req = KnHttp.get('/api/users')
	.onProgress(progress => {})       // Upload / download progress
	.onSuccess(res => {})             // Success
	.onError(err => {})               // Error
	.onEnd(wasSuccess => {});         // Always called last

req.abort();

Promise (async / await)

The same request object is awaitable: it is resolved with the success response and rejected with a KnHttpError. The Promise is only created on the first await, then, catch or finally (no "unhandled rejection" with the callbacks only).

try {
	const { data, status, headers } = await KnHttp.get('/api/users');
} catch (err) {
	if (KnHttp.isCanceled(err)) return; // Aborted request
	console.error(err.code, err.status, err.message);
}

// Parallel requests
const [users, groups] = await Promise.all([KnHttp.get('/api/users'), KnHttp.get('/api/groups')]);

// Callbacks and await on the same request, e.g. to follow the upload progress
const res = await KnHttp.post('/api/upload', { formData: { file }, upload: true })
	.onProgress(progress => progressBar.value = progress);

Instances

KnHttp is the default instance. KnHttp.create(config) creates an instance with its own default options (merged with the defaults of the parent instance, the headers and the hooks are merged too):

const api = KnHttp.create({
	baseUrl: 'https://example.com/api',
	timeout: 30_000,
	headers: { 'Accept-Language': 'fr' },
	authToken: () => store.token
});

await api.get('/users');                                   // https://example.com/api/users
await api.get('https://other.com/data');                   // Absolute URL, the base URL is not used
const admin = api.create({ headers: { 'X-Admin': '1' } }); // Instance of an instance

The relative URLs (with or without / at the beginning) are joined with baseUrl, the URLs with a scheme (https://...) or protocol relative (//...) are not.

Query string parameters

await KnHttp.get('/api/users', { params: { page: 2, sort: 'name', filter: { active: true }, ids: [1, 2] } });
// /api/users?page=2&sort=name&filter[active]=true&ids[0]=1&ids[1]=2

Authentication

// Token (Authorization: Bearer <token> by default), the value can be a function evaluated at each request
KnHttp.defaults.authToken = () => localStorage.getItem('token');

// Other scheme: Authorization: Token <token>
await KnHttp.get('/api/me', { authToken: token, authScheme: 'Token' });

// Other header without scheme: X-API-Key: <token>
const api = KnHttp.create({ authToken: apiKey, authHeader: 'X-API-Key', authScheme: null });

// HTTP basic auth (UTF-8 credentials supported)
await KnHttp.get('/api/me', { basicAuth: { username: 'user', password: 'pass' } });

// CSRF token (X-CSRF header by default), the value can be a function
KnHttp.defaults.csrf = () => document.querySelector('meta[name=csrf-token]').content;

// Disable a default value for one request
await KnHttp.get('https://other.com/public', { authToken: null, csrf: null });

Hooks

The hooks are defined on an instance (create({ hooks }) or defaults.hooks):

let token = 'expired';

const api = KnHttp.create({
	baseUrl: '/api',
	authToken: () => token,
	hooks: {
		// Before each attempt (can be async): update ctx.url, ctx.method or ctx.headers (lower case names)
		beforeRequest: async ctx => {
			ctx.headers['x-request-id'] = crypto.randomUUID();
		},
		// After each success response
		afterResponse: (res, ctx) => {
			console.log(ctx.method, ctx.url, res.status);
		},
		// Before each error (can be async): return true to retry the request
		beforeError: async (err, ctx) => {
			if (err.status == 401 && ctx.attempt == 0) {
				token = await refreshToken();
				return true;
			}
		}
	}
});

ctx.attempt is 0 for the first attempt, then 1, 2... Check it in beforeError to avoid infinite retries.

Retry

The network errors, the timeouts and some HTTP errors can be retried automatically:

// 3 retries with the default options
await KnHttp.get('/api/data', { retry: 3 });

// Custom retry options
const api = KnHttp.create({
	retry: {
		limit: 3,                      // Maximum number of retries
		methods: ['GET', 'PUT'],       // Default: GET, HEAD, OPTIONS, PUT, DELETE
		statusCodes: [502, 503],       // Default: 408, 429, 500, 502, 503, 504
		delay: retry => 1000 * retry   // Default: 300 ms, 600 ms, 1200 ms...
	}
});

POST and PATCH are not retried by default (not idempotent).

Abort

// With the request object
const req = KnHttp.get('/api/search?q=test');
req.abort();

// With an AbortSignal
const controller = new AbortController();
KnHttp.get('/api/search?q=test', { signal: controller.signal });
controller.abort();

An aborted request ends with a KnHttp.CANCELED_ERROR error (onError then onEnd(false), or a rejection).

Unhandled errors

defaults.onUnhandledError is called for the errors of the requests without onError callback and not awaited, e.g. a default error message for the requests that only use onSuccess:

KnHttp.defaults.onUnhandledError = err => {
	if (err.code != KnHttp.CANCELED_ERROR) notifyError(err.message);
};

KnHttp.get('/api/stats').onSuccess(res => showStats(res.data)); // An error calls onUnhandledError

Use the beforeError hook to act on the errors of all the requests (redirect to the login page on 401...).

Reactive states (Vue, Pinia...)

The instances and the request objects are never made reactive by Vue (they have an internal state): they can be stored in a ref, a reactive object or a Pinia store.

const search = ref(null);

function onInput(q) {
	search.value?.abort();
	search.value = KnHttp.get('/api/search', { params: { q } }).onSuccess(res => results.value = res.data);
}

API

Request methods

All the methods return a request object. The generic type <T> is TypeScript only (see Typed response data).

Method HTTP method Options
KnHttp.get<T>(url, opt?) GET Options
KnHttp.post<T>(url, opt?) POST Options + body
KnHttp.put<T>(url, opt?) PUT Options + body
KnHttp.patch<T>(url, opt?) PATCH Options + body
KnHttp.delete<T>(url, opt?) DELETE Options + body
KnHttp.request<T>(url, opt?) opt.method (GET by default) Options + body + method
KnHttp.download(url, opt?) GET Options, the response is saved as a file by the browser (res.data is true)

Request object

const req = KnHttp.get('/api/users');

req.onProgress(progress => {});   // Progress in pourcent (-1 if not computable): upload option or download
req.onSuccess(res => {});         // Success
req.onError(err => {});           // Error (the error is handled: onUnhandledError is not called, even with null)
req.onEnd(wasSuccess => {});      // Always called last, whatever the result
req.abort();                      // Abort (do nothing if the request is already ended)
req.xhr;                          // XMLHttpRequest of the request
req.ended;                        // True once the request is ended

await req;                        // Promise: resolved with the response, rejected with a KnHttpError
req.then(res => {}, err => {});   // then / catch / finally
  • All the on* methods return the request object (chaining), pass null to remove a callback
  • The callbacks are called in this order: onSuccess then onEnd(true), or onError then onEnd(false)
  • An exception thrown in a callback is reported in the console without breaking the request

Success response

Property Type Description
res.data T Response body according to the response type (null if the response is empty)
res.headers object Response headers
res.status number HTTP status

Error

The error is a KnHttpError, it extends the native Error.

Property Type Description
err.code string Error code, see below
err.status number HTTP status (0 if there is no response)
err.data unknown Response body (parsed JSON if possible, text otherwise), null if there is no response
err.headers object Response headers
err.message string "Request canceled", "Network error", "Request timeout", "HTTP error 500", "Invalid response" or "Unknown error"
err.cause unknown Original error (JSON SyntaxError, exception of the parse option or of a hook)

Error codes

Constant Value Description
KnHttp.CANCELED_ERROR 'canceled' The request has been aborted (req.abort() or signal)
KnHttp.NETWORK_ERROR 'network' Network error (server unreachable, no internet, CORS...)
KnHttp.TIMEOUT_ERROR 'timeout' Timeout
KnHttp.HTTP_ERROR 'http' Invalid HTTP status (validateStatus, not 2xx by default)
KnHttp.PARSE_ERROR 'parse' Invalid JSON or parse option failed
KnHttp.UNKNOWN_ERROR 'unknown' Other error (exception thrown in a hook...)
KnHttp.isKnHttpError(err); // True if err is a KnHttpError
KnHttp.isCanceled(err);    // True if err is a KnHttpError with the CANCELED_ERROR code

Options

The options are the last parameter of the request methods. An option not set uses the default value of the instance (default options).

Option Type Description
params object Query string parameters (nested objects and arrays as key[subkey])
headers object Request headers (case insensitive, they override the default headers)
responseType 'json', 'text', 'blob', 'arraybuffer', 'document' Response type
parse (data) => T Parse / validate the response data, the returned value is res.data (an exception ends with a PARSE_ERROR)
timeout number Timeout in ms (0 = no timeout)
retry number or object Retries, see Retry
signal AbortSignal Abort the request
authToken string, () => string or null Auth token (null disables the default token)
authHeader string Auth header name
authScheme string or null Auth scheme (null or empty = the token only)
basicAuth { username, password } or null HTTP basic auth
csrf string, () => string or null CSRF token
csrfHeader string CSRF header name
withCredentials boolean XMLHttpRequest.withCredentials (cookies for cross-origin requests)
upload boolean Enable the upload progress (onProgress), forces a CORS preflight for cross-origin requests
json, form, formData, body See below Request body (not for get)
method string HTTP method (request only)

Request bodies

One body option per request (post, put, patch, delete and request):

Option Accepted values Sent as
json Any JSON serializable value application/json
form Object (strings, numbers, booleans, null, arrays, nested objects) application/x-www-form-urlencoded
formData Same as form, plus File and Blob multipart/form-data
body string, Blob, ArrayBuffer, typed array, FormData, URLSearchParams, Document As is

For form and formData, the nested objects and arrays are serialized as key[subkey] and the null / undefined values are sent as an empty string:

await KnHttp.post('/api/upload', {
	formData: {
		id: 42,                        // id=42
		tags: ['a', 'b'],              // tags[0]=a, tags[1]=b
		filter: { active: true },      // filter[active]=true
		comment: null,                 // comment=
		file: fileInput.files[0],      // File
		thumbnail: new Blob([content]) // Blob
	}
});

Default options

KnHttp.defaults (or api.defaults for an instance) can be updated, KnHttp.create(config) accepts the same properties.

Property Default Description
baseUrl '' Base URL of the relative URLs
validateStatus status => status >= 200 && status < 300 Valid HTTP status (success or HTTP_ERROR)
onUnhandledError null Unhandled error callback, see Unhandled errors
hooks {} Hooks, see Hooks
retry 0 Retries, see Retry
timeout 270000 Timeout in ms
authToken null Auth token (string or function)
authHeader 'Authorization' Auth header name
authScheme 'Bearer' Auth scheme
basicAuth null HTTP basic auth
csrf null CSRF token (string or function)
csrfHeader 'X-CSRF' CSRF header name
withCredentials false XMLHttpRequest.withCredentials
responseType 'json' Response type
headers { 'Accept': '*/*', 'X-Requested-With': 'XMLHttpRequest' } Headers added to all the requests

The X-Requested-With default header is only sent to the same origin as the page: it is not a CORS safe header, it would add a preflight request (OPTIONS) to each cross-origin request. Set it in the headers option of the request to force it.

Properties

KnHttp.VERSION;  // Lib version
KnHttp.defaults; // Default options

TypeScript

Typed response data

The type of res.data depends on the options:

interface User {
	id: number;
	name: string;
}

const user = await KnHttp.get<User>('/api/users/42');                     // res.data: User
const raw = await KnHttp.get('/api/users/42');                            // res.data: unknown (must be typed or checked)
const text = await KnHttp.get('/robots.txt', { responseType: 'text' });   // res.data: string
const blob = await KnHttp.get('/logo.png', { responseType: 'blob' });     // res.data: Blob
const saved = await KnHttp.download('/export.csv');                       // res.data: true

Runtime validation (optional)

The generic type is not checked at runtime. The optional parse option validates (or transforms) the response, its return type is the type of res.data. It works with any validation library (Zod, Valibot...) or a custom function, without dependency:

import { z } from 'zod';

const User = z.object({ id: z.number(), name: z.string() });

const res = await KnHttp.get('/api/users/42', { parse: data => User.parse(data) }); // res.data: { id: number; name: string }

// Without library
const count = await KnHttp.get('/api/count', {
	parse: data => {
		if (typeof data != 'number') throw new TypeError('Invalid count');
		return data;
	}
});

An exception thrown by parse ends the request with a KnHttp.PARSE_ERROR error (the exception is in err.cause).

Typed bodies and options

The bodies and the options are checked at compile time:

KnHttp.post('/url', { json: data, form: data });            // Error: one body per request
KnHttp.get('/url', { json: data });                         // Error: no body for GET
KnHttp.post('/url', { form: { date: new Date() } });        // Error: Date is not a form value
KnHttp.post('/url', { form: { file: file } });              // Error: File is only allowed in form data
KnHttp.get('/url', { responseType: 'xml' });                // Error: invalid response type
KnHttp.get('/url', { timout: 1000 });                       // Error: unknown option

A body typed with an interface is not accepted as a form or formData body (TypeScript does not add an index signature to interfaces). Use a type instead, or spread the object:

interface Params { q: string; page: number; }
const params: Params = { q: 'test', page: 1 };

KnHttp.post('/search', { form: { ...params } });

Exported types

import type { KnHttpRequest, KnHttpResponse, KnHttpError, KnHttpOptions } from 'kn-http';
Type Description
KnHttpClient Type of KnHttp and of the instances
KnHttpRequest<T> Request object
KnHttpResponse<T> Success response
KnHttpError Error, extends Error
KnHttpErrorCode 'canceled' | 'network' | 'timeout' | 'http' | 'parse' | 'unknown'
KnHttpBaseOptions Options of all the requests
KnHttpOptions KnHttpBaseOptions with responseType and parse
KnHttpBodyOptions Body options (json, form, formData, body)
KnHttpSendOptions Options of post, put, patch and delete
KnHttpRequestOptions Options of request
KnHttpDefaults, KnHttpConfig Default options and config of create
KnHttpHooks, KnHttpHookContext Hooks and their context
KnHttpRetryOptions Retry options
KnHttpMethod, KnHttpResponseType, KnHttpHeaders, KnHttpParams, KnHttpBasicAuth, KnHttpValue<T> Options values
KnHttpRawBody, KnHttpFormBody, KnHttpFormValue, KnHttpFormDataBody, KnHttpFormDataValue Bodies
KnHttpProgressCallback, KnHttpSuccessCallback<T>, KnHttpErrorCallback, KnHttpEndCallback Callbacks

Upgrading from v3

The global KnHttp, the file names, the request object and its callbacks (onProgress, onSuccess, onError, onEnd, abort) are unchanged. The other changes:

v3 v4
KnHttp.getText(url, opt) KnHttp.get(url, { ...opt, responseType: 'text' })
KnHttp.del(url, opt) KnHttp.delete(url, opt)
KnHttp.postRaw(url, data, opt) / putRaw KnHttp.post(url, { ...opt, body: data }) / put
KnHttp.postForm(url, data, opt) / putForm KnHttp.post(url, { ...opt, form: data }) / put
KnHttp.postFormData(url, data, opt) / putFormData KnHttp.post(url, { ...opt, formData: data }) / put
KnHttp.postJson(url, data, opt) / putJson KnHttp.post(url, { ...opt, json: data }) / put
KnHttp.request(url, method, { requestType, body }) KnHttp.request(url, { method, json / form / formData / body })
KnHttp.DEFAULTS KnHttp.defaults
KnHttp.DEFAULTS.onError KnHttp.defaults.onUnhandledError (not called for the awaited requests)
KnHttp.DEFAULTS.requestType Removed (body options)
bearerAuthToken option authToken option
res.httpCode / err.httpCode res.status / err.status
req._xhr req.xhr
Numeric error codes (-1, 0, 1, 2) String error codes, compare with the constants (KnHttp.HTTP_ERROR...)
Timeout: UNKNOWN_ERROR TIMEOUT_ERROR
Invalid JSON: UNKNOWN_ERROR PARSE_ERROR
Success: HTTP status 200 only All the 2xx HTTP status, empty responses (204...) with res.data = null
JSON null, false, 0, "": error or success Success

Other changes:

  • err.data is the parsed JSON of the error response (when possible)
  • The X-Requested-With default header is no longer sent to the other origins
  • The error object is an instance of Error (with message, name, stack and cause)
  • The bodies false, 0 and "" are sent, an option set to undefined uses the default value
  • TypeScript: the Deferred, Response and Error types are renamed KnHttpRequest, KnHttpResponse and KnHttpError, res.data is unknown by default, the declarations are in dist/types/
  • The minimum browser versions are higher, see Browser support

See the changelog for all the changes.

Browser support

  • Chrome / Edge 85+
  • Firefox 79+
  • Safari 14.1+

Build

The sources are in src (TypeScript 6, strict mode), bundled with Vite 8 (library mode), the declarations are generated with tsc.

# Install the dev dependencies
pnpm install

# Type check the sources
pnpm typecheck

# Type check, build the bundles and the declarations into dist
pnpm build

# Rebuild the bundles on each change
pnpm dev

Changelog

See the changelog here

License

See the license here

The MIT License (MIT)

Copyright (c) 2022-2026 Florent VIALATTE

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

About

Lightweight HTTP client for the browser (based on XMLHttpRequest), written in TypeScript.

Topics

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Used by

Contributors

Languages