npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

bun-rift

v0.1.0

Published

A Fetch API implementation for Bun with extended features and improved security.

Downloads

18

Readme

bun-rift

npm

bun

Zero-dependency HTTP client for Bun with retries, interceptors and automatic transforms.

bun add bun-rift

Requires 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 onRetry hook
  • 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 message
  • config: The request config
  • request: The Bun Request object
  • response: 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 thrown RiftError as error.data. If parsing fails (for example invalid JSON) the attached value will be null. This makes it safe to inspect error.data without risking additional parse exceptions during error handling.
  • error.responseType: When available, the error exposes a responseType hint (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.