Lightweight HTTP client for the browser (based on XMLHttpRequest), written in TypeScript.
async/awaitand chainable callbacks (onSuccess,onError,onEnd) on the same request objectget,post,put,patch,deletewithjson,form,formData(withFile/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...)
- Installation
- Quick start
- Usage
- API
- TypeScript
- Upgrading from v3
- Browser support
- Build
- Changelog
- License
pnpm add kn-httpnpm install kn-httpThe TypeScript declarations are included in the package, nothing else to install.
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.
| 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 |
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;
}
}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).
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>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();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);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 instanceThe relative URLs (with or without / at the beginning) are joined with baseUrl, the URLs with a scheme (https://...) or protocol relative (//...) are not.
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// 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 });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.
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).
// 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).
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 onUnhandledErrorUse the beforeError hook to act on the errors of all the requests (redirect to the login page on 401...).
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);
}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) |
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), passnullto remove a callback - The callbacks are called in this order:
onSuccessthenonEnd(true), oronErrorthenonEnd(false) - An exception thrown in a callback is reported in the console without breaking the request
| 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 |
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) |
| 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 codeThe 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) |
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
}
});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.
KnHttp.VERSION; // Lib version
KnHttp.defaults; // Default optionsThe 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: trueThe 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).
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 optionA 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 } });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 |
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.datais the parsed JSON of the error response (when possible)- The
X-Requested-Withdefault header is no longer sent to the other origins - The error object is an instance of
Error(withmessage,name,stackandcause) - The bodies
false,0and""are sent, an option set toundefineduses the default value - TypeScript: the
Deferred,ResponseandErrortypes are renamedKnHttpRequest,KnHttpResponseandKnHttpError,res.dataisunknownby default, the declarations are indist/types/ - The minimum browser versions are higher, see Browser support
See the changelog for all the changes.
- Chrome / Edge 85+
- Firefox 79+
- Safari 14.1+
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 devSee the changelog here
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.