@cassets/http-client
v0.1.1
Published
Fetch-based TypeScript HTTP client with interceptors, retries, React hooks, and browser-native chunked file uploads.
Downloads
57
Maintainers
Readme
@cassets/http-client
A fetch-based TypeScript HTTP client with request/response interceptors, retries, timeouts, React helpers, and browser-native chunked uploads.
@cassets/http-client is intentionally small: it wraps the platform fetch API instead of replacing it, while adding reusable behavior needed by application and upload layers.
Installation
Core only:
npm install @cassets/http-clientReact hooks/provider:
npm install @cassets/http-client reactReact is optional and is declared as a peer dependency.
Runtime support
The core HTTP client requires a runtime with fetch, Headers, AbortController, and standard Fetch API body types. Chunked uploads additionally require browser APIs including File, Blob, Worker, XMLHttpRequest, and localStorage when persistence metadata is enabled.
Quick start
import { HttpClient } from '@cassets/http-client';
const api = new HttpClient({
baseURL: 'https://api.example.com/',
defaultHeaders: {
Accept: 'application/json',
},
timeout: 10_000,
});
const response = await api.get<{ id: string; name: string }>('/users/42');
console.log(response.status, response.data);Request bodies are passed to
fetchas provided. For JSON, useJSON.stringify()and setContent-Type: application/json.
await api.post(
'/users',
JSON.stringify({ name: 'Vivek' }),
{ headers: { 'Content-Type': 'application/json' } },
);Response shape
interface HttpResponse<T> {
data: T;
status: number;
statusText: string;
headers: Headers;
config: HttpRequest;
}JSON responses are parsed as JSON, text/* as text, and other content types as Blob.
Like fetch, non-2xx HTTP status codes are returned as responses; they do not automatically throw. Network failures and aborts reject the promise.
Convenience methods
api.get('/users');
api.post('/users', body);
api.put('/users/42', body);
api.patch('/users/42', body);
api.delete('/users/42');Each method accepts an optional request config.
const controller = new AbortController();
api.get('/reports', {
signal: controller.signal,
timeout: 30_000,
headers: { 'X-Request-ID': crypto.randomUUID() },
});
controller.abort();Request interceptors
api.use(async (request) => {
return {
...request,
headers: {
...request.headers,
'X-Client-Version': 'web-1.0',
},
};
});Interceptors run in registration order.
Response interceptors
Register a response interceptor using the second true argument:
api.use(async (response) => {
if (response.status === 401) {
// application-specific handling
}
return response;
}, true);Authentication interceptor
import { HttpClient, authInterceptor } from '@cassets/http-client';
const api = new HttpClient({ baseURL: 'https://api.example.com/' });
api.use(authInterceptor(async () => {
return sessionStorage.getItem('access_token') ?? '';
}));Custom header/scheme:
api.use(authInterceptor(getApiKey, 'X-API-Key', ''));Retry configuration
Global retry policy:
const api = new HttpClient({
retries: 3,
retryDelay: (attempt) => Math.min(500 * 2 ** attempt, 5_000),
onRetry: (error, attempt) => {
console.warn('retry', attempt, error.message);
},
});Per-request via interceptor:
import { retryInterceptor } from '@cassets/http-client';
api.use(retryInterceptor(2, (attempt) => attempt * 750));Current retry behavior applies to rejected fetch/network attempts. HTTP responses such as 429 or 503 are still returned normally; implement status-based retry policy in your application if required.
Logging interceptors
import {
loggingInterceptor,
responseLoggingInterceptor,
setLogLevel,
} from '@cassets/http-client';
setLogLevel('debug');
api.use(loggingInterceptor());
api.use(responseLoggingInterceptor(), true);Authorization, cookie, and x-api-key request/response headers are redacted by the built-in logger. Application payloads are not automatically redacted, so do not enable debug logging for sensitive bodies in production.
Levels:
setLogLevel('debug');
setLogLevel('info');
setLogLevel('warn');
setLogLevel('error');
setLogLevel('none');React: useHttp
import { useHttp } from '@cassets/http-client/react';
export function Users() {
const api = useHttp({ baseURL: 'https://api.example.com/' });
async function load() {
const response = await api.get('/users');
console.log(response.data);
}
return <button onClick={load}>Load users</button>;
}useHttp creates one client instance for the mounted component. Configuration is used at initial creation.
Chunked browser uploads
The upload layer splits a File into chunks, requests signed upload URLs from your callback, and uploads chunks from a Web Worker with configurable parallelism.
import { UploadManager } from '@cassets/http-client/upload';
const manager = UploadManager.getInstance();
const result = await manager.upload({
file,
chunkSize: 10 * 1024 * 1024,
parallelParts: 3,
getUploadUrls: async (file, chunkSize, totalParts) => {
const response = await fetch('/api/uploads/sign', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
fileName: file.name,
fileSize: file.size,
contentType: file.type,
chunkSize,
totalParts,
}),
});
const data = await response.json();
return data.urls;
},
onProgress: ({ percent, speed, eta }) => {
console.log(`${percent.toFixed(1)}%`, speed, eta);
},
});The signer must return:
Array<{
partNumber: number;
url: string;
uploadId?: string;
}>Using @cassets/cloud
The cloud package provides signer-callback adapters with the exact getUploadUrls contract:
npm install @cassets/http-client @cassets/cloudimport { UploadManager } from '@cassets/http-client/upload';
import { createS3UploadUrlFetcher } from '@cassets/cloud/s3';
const getUploadUrls = createS3UploadUrlFetcher({
signEndpoint: '/api/uploads/s3/sign',
headers: {
Authorization: `Bearer ${token}`,
},
});
await UploadManager.getInstance().upload({
file,
getUploadUrls,
});The same pattern works for Azure Blob Storage and Google Cloud Storage.
Upload progress
interface UploadProgress {
file: File;
uploadedBytes: number;
totalBytes: number;
percent: number;
speed?: number;
eta?: number;
completedParts: number;
totalParts: number;
state: 'idle' | 'uploading' | 'paused' | 'completed' | 'error' | 'cancelled';
}Progress includes in-flight XHR bytes, so percentage can update inside a chunk rather than only when a chunk completes.
Pause, resume, cancel
UploadManager exposes task controls by task ID:
manager.pause(taskId);
manager.resume(taskId);
manager.cancel(taskId);Important semantics in 0.1.x:
- Pause is soft: it prevents new chunks from starting; chunks already in flight may finish.
- Resume: starts pending chunks again.
- Cancel: terminates the worker and rejects the upload promise.
- A failed chunk currently fails the complete upload; automatic per-chunk retries are not yet implemented.
If your UI needs the task ID before completion, construct UploadTask directly or obtain active tasks from the manager.
React upload provider
import { UploadProvider, useUpload } from '@cassets/http-client/react';
function App() {
return (
<UploadProvider>
<Uploader />
</UploadProvider>
);
}
function Uploader() {
const { upload, pause, resume, cancel, tasks } = useUpload();
async function start(file: File) {
await upload({
file,
getUploadUrls,
});
}
return <div>Active uploads: {tasks.length}</div>;
}The provider refreshes its active-task snapshot periodically.
Persistence metadata
Set persistKey to store upload progress metadata in localStorage:
await manager.upload({
file,
getUploadUrls,
persistKey: `upload:${file.name}:${file.size}`,
});In 0.1.x, this is checkpoint metadata only. It does not guarantee cross-page/restart multipart resume; after reload the upload is reconciled by re-requesting URLs and may re-upload chunks. Do not market it as durable resumability yet.
File hashing
import { computeFileHash } from '@cassets/http-client/upload';
const sha256 = await computeFileHash(file);This currently reads the complete file into memory before hashing. For multi-gigabyte files, use a streaming/incremental hashing strategy in the application instead.
Upload security checklist
- Generate signed URLs on a trusted backend; never expose cloud secret keys in the browser.
- Keep signed URLs short-lived and scope them to one object/part.
- Validate file name, size, MIME type, tenant, user, and authorization server-side.
- Configure storage CORS only for required origins and methods.
- Treat client-side hashes as integrity hints unless the server/cloud validates them.
- Complete multipart uploads server-side where the provider requires a finalize/commit call; this package currently uploads parts but does not own provider-specific completion APIs.
Important provider note
For S3 multipart upload, Azure block blobs, and some GCS flows, uploading signed parts is only one stage. Your backend may need to create the multipart session and later complete/commit it. @cassets/http-client deliberately leaves those provider-specific lifecycle calls to your backend/application.
Package exports
import { HttpClient } from '@cassets/http-client';
import { useHttp, UploadProvider, useUpload } from '@cassets/http-client/react';
import { UploadManager, UploadTask, computeFileHash } from '@cassets/http-client/upload';Both ESM and CommonJS builds plus TypeScript declarations are published.
Project links
- ClusterAssets GitHub: https://github.com/clusterassets
- ClusterAssets LinkedIn: https://www.linkedin.com/company/clusterassets
- Creator GitHub: https://github.com/diskhacker
- Creator LinkedIn: https://www.linkedin.com/in/kp-vivek-rao-bhosale/
Contributing and security
See CONTRIBUTING.md and SECURITY.md.
License
MIT © Vivek Rao Bhosale / ClusterAssets.
