bun-rift
v0.1.0
Published
A Fetch API implementation for Bun with extended features and improved security.
Downloads
18
Maintainers
Readme
bun-rift
Zero-dependency HTTP client for Bun with retries, interceptors and automatic transforms.
bun add bun-riftRequires Bun ≥ 1.3.
Usage
import { Rift } from 'bun-rift';
const api = new Rift({
baseURL: 'https://api.example.com',
timeout: 5_000,
});
const users = await api.get<User[]>('/users');
console.log(users.data);Features
- Retries with exponential back-off, idempotency rules and
onRetryhook - Request / Response interceptors (add auth, logging, etc.)
- Automatic FormData for
postForm,putForm,patchForm - Tiny – no external dependencies, < 10 kB
- Fully typed – written in TypeScript, ships with types
API
new Rift(config?)
Create an instance. All options are optional.
| Option | Type | Default | Description | | :---------------- | :-----------------------------------: | -----------------------: | -----------------------: | | baseURL | string | - | Prefix for all URLs | | timeout | number | - | ms until abort | | retry | number | 0 | Max retry attempts | | retryDelay | number or (attempt: number) => number | 1000 | Delay between retries | | retryOnMethods | string[] | ['GET','HEAD','OPTIONS'] | Idempotent verbs | | validateStatus | (status: number) => boolean | 200-299 | Resolve vs throw | | transformRequest | ((data: any) => any)[] | [] | Request body transforms | | transformResponse | ((data: any) => any)[] | [] | Response body transforms | | logger | (level: string, info: any) => void | - | Log messages | | onRetry | (attempt: number, error: any) => void | - | Called before retry |
HTTP Methods
api.get<TData, TResponse>(url: string, config?: RequestConfig<TData>):Promise<TResponse>
api.post<TData, TResponse>(url: string, data?: TData, config?: RequestConfig<TData>):Promise<TResponse>
api.put<TData, TResponse>(url: string, data?: TData, config?: RequestConfig<TData>):Promise<TResponse>
api.patch<TData, TResponse>(url: string, data?: TData, config?: RequestConfig<TData>):Promise<TResponse>
api.delete<TData, TResponse>(url: string, config?: RequestConfig<TData>):Promise<TResponse>
api.head<TData, TResponse>(url: string, config?: RequestConfig<TData>):Promise<TResponse>
api.options<TData, TResponse>(url: string, config?: RequestConfig<TData>):Promise<TResponse>FormData Methods
api.postForm<TData, TResponse>(url: string, data?: Record<string, string>, config?: RequestConfig<TData>):Promise<TResponse>
api.putForm<TData, TResponse>(url: string, data?: Record<string, string>, config?: RequestConfig<TData>):Promise<TResponse>
api.patchForm<TData, TResponse>(url: string, data?: Record<string, string>, config?: RequestConfig<TData>):Promise<TResponse>Files and Blobs are appended as-is. Arrays are supported — primitive items are coerced to strings, File/Blob items are preserved, and null/undefined array elements are skipped. This avoids runtime differences across fetch implementations.
Interceptors
Request interceptors
const remove = api.useRequestInterceptor((config) => {
config.headers = { ...config.headers, Authorization: `Bearer ${token}` };
return config;
});
// Later
remove();Response interceptors
useResponseInterceptor accepts both a fulfilled handler and an optional rejected (error) handler: useResponseInterceptor(onFulfilled?, onRejected?).
// Modify successful responses
api.useResponseInterceptor((response) => {
response.data.modified = true;
return response;
});
// Recover from errors (e.g., fallback) or rethrow to propagate
api.useResponseInterceptor(undefined, (error) => {
// Return a valid response to recover from the error
// @ts-ignore minimal test response
return {
data: { recovered: true },
status: 200,
statusText: 'OK',
headers: new Headers(),
config: {},
};
});Rejected handlers are executed in reverse registration order and may either return a valid RiftResponse to recover, or throw / return a rejected promise to continue the error chain. If no rejected handler recovers, the original error is thrown (wrapped in RiftInterceptorError when appropriate).
Retry Logic
By default, only idempotent HTTP methods are retried (GET, HEAD, OPTIONS). You can customize this with the retryOnMethods option.
await api.get('/unstable', {
retry: 3,
retryDelay: (attempt) => attempt * 200,
onRetry: ({ attempt, maxRetries, delay, error }) => {
console.warn(`Retry ${attempt}/${maxRetries} in ${delay}ms`, error.message);
},
});Errors
All errors are instances of RiftError.
When a request fails, an error is thrown with the following properties:
message: Error messageconfig: The request configrequest: The Bun Request objectresponse: The Bun Response object (if available)
try {
await api.get('/not-found');
} catch (error) {
if (error instanceof RiftError) {
console.error('Request failed with status:', error.response?.status);
}
}Additional error details
error.data: When an HTTP error is thrown the client makes a best-effort attempt to parse the response body and attach the parsed payload to the thrownRiftErroraserror.data. If parsing fails (for example invalid JSON) the attached value will benull. This makes it safe to inspecterror.datawithout risking additional parse exceptions during error handling.error.responseType: When available, the error exposes aresponseTypehint (from the request config or response) indicating how the body was parsed.
Helper and usage
The library exposes a helper createRiftErrorFromResponse for adapters or custom code that parse response bodies and want to throw typed errors with the parsed payload attached. Example:
import { createRiftErrorFromResponse } from 'bun-rift';
const parsed = await parseResponse<MyErrorShape>(res, 'json');
throw createRiftErrorFromResponse('Bad response', config, {
data: parsed,
status: res.status,
statusText: res.statusText,
headers: res.headers,
config,
});URL Builder
const url = api.getUri({
url: '/users',
params: { search: 'john', limit: 10 },
});
// url = 'https://api.example.com/users?search=john&limit=10'Contributing
Contributions are welcome! Please open an issue or submit a pull request on GitHub.
License
Licensed under the MIT License.
