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

@hootool/http-client

v1.0.3

Published

hootool http-client

Readme

@hootool/http-client

Koa 风格中间件的 fetch 客户端,可选接入 TanStack Query 与 WebSocket。

NPM version

安装

pnpm add @hootool/http-client

使用 react-query / hook 时再装 peer:

pnpm add @tanstack/react-query react

| 入口 | 用途 | |---|---| | @hootool/http-client | createHttp、中间件、ApiError | | @hootool/http-client/react-query | createFetcher / createMutationFn | | @hootool/http-client/websocket | WebSocketService | | @hootool/http-client/use-web-socket | useWebSocket |

HttpClient

import { createHttp, ApiError, TimeoutError } from '@hootool/http-client';

const http = createHttp({
  baseURL: '/api',
  timeout: 10_000,
  headers: { 'X-App': 'web' },
});

const list = await http.get<{ items: string[] }>('/users', { page: 1, ids: [1, 2] });
await http.post('/users', { name: 'sai' });
await http.put('/user/1', { name: 'sai' });
await http.patch('/user/1', { name: 'sai' });
await http.delete('/item', { id: 1 });
await http.head('/item', { id: 1 });

const file = await http.get<Blob>('/report.pdf', undefined, { responseType: 'blob' });
const child = http.create({ baseURL: '/upload' });
  • get / delete / head:(path, params?, options?)
  • post / put / patch:(path, data?, options?);FormData 原样发送
  • timeout 为 0 或不传表示不限制(仍可用 signal);超时抛 TimeoutError
  • http.create() 派生实例,浅拷贝 defaults 与中间件

responseType

| 值 | 行为 | |---|---| | auto(默认) | 按 Content-Type:json → json();无 CT 先试 JSON,失败当文本;text/* / xml → text;其余 → blob | | json | 强制 response.json() | | text / blob / arrayBuffer / formData | 对应原生解析 |

中间件

洋葱模型,先注册的更靠外。metadata 只给中间件 / 业务用,不进 fetch。

http.use(async (ctx, next) => {
  ctx.metadata.traceId = 'x';
  ctx.init.headers = { ...ctx.init.headers, Authorization: 'Bearer token' };
  await next();
  console.log(ctx.data, ctx.json); // json 为对象或数组
});

扩展 metadata 类型:

declare module '@hootool/http-client' {
  interface RequestMetadata {
    traceId?: string;
  }
}

错误

HTTP 非 2xx 抛 ApiError,只带 status 与 statusText,不解析错误 body。业务信封(200 + 业务码)用 createBizCodeMiddleware:

import {
  createHttp,
  ApiError,
  TimeoutError,
  BizError,
  createBizCodeMiddleware,
  createErrorMiddleware,
  isAbortWithoutTimeout,
} from '@hootool/http-client';

const http = createHttp({
  middlewares: [
    createErrorMiddleware({
      handleError: (error) => {
        if (error instanceof ApiError && error.status === 401) logout();
      },
      showError: (error, ctx) => {
        if (isAbortWithoutTimeout(error, ctx)) return;
        if (error instanceof TimeoutError) toast('超时');
        else if (error instanceof ApiError) toast(`网络错误 ${error.status}`);
        else if (error instanceof BizError) toast(error.message);
      },
    }),
    createBizCodeMiddleware({
      codeField: 'code',      // 默认
      messageField: 'message', // 默认
      successCode: ['0', '0000'],
    }),
  ],
});

// 关闭本次校验
http.get('/export', undefined, { metadata: { successCode: false } });
// 覆盖本次成功码(会强转 string)
http.get('/special', undefined, { metadata: { successCode: [1, '00'] } });

默认读 code / message 并强转 string,成功码默认 0。成功时返回完整 JSON,不拆 data。需要内层数据时用 transform 或自己取。

createErrorMiddleware 须放在最外。handleError 每次失败都会调(401 登出等);showError 受中间件 defaultShowError 与 metadata.showError 控制。metadata.showError 可为 false / true,或传入函数覆盖全局提示。取消是否提示由业务用 isAbortWithoutTimeout 自行判断。超时是 TimeoutError,isAbortWithoutTimeout 为 false。React Query 的 CancelledError 在 query 层,不走这条。

createErrorMiddleware({ defaultShowError: false, showError }); // 默认不提示,handleError 仍会调
http.get('/poll', undefined, { metadata: { showError: false } }); // 本次不提示
http.get('/pay', undefined, { metadata: { showError: true } });  // 本次强制用全局 showError
http.get('/upload', undefined, { metadata: { showError: (e) => toast(String(e)) } }); // 本次自定义提示
try {
  await http.get('/users');
} catch (e) {
  if (e instanceof ApiError) console.log(e.status);
  if (e instanceof TimeoutError) console.log(e.timeout);
  if (e instanceof BizError) console.log(e.code, e.message);
}

react-query

import { createReactQueryProvider } from '@hootool/http-client/react-query';

const { createFetcher, createMutationFn, runMutation, toQueryFn } = createReactQueryProvider({
  request: (path, options) => http.request(path, options),
  queryClient,
});

const getUser = createFetcher<User, { id: string }>('/users/:id');
const createUser = createMutationFn<User, { name: string }>('/users', { method: 'POST' });

useQuery({ queryKey: ['user', id], queryFn: toQueryFn(getUser, { id }) });
await runMutation(createUser, { name: 'sai' });
  • path 支持 :id / {id},用过的 key 从 variables 里剔除,不再进 query/body
  • 函数 path 也要返回带占位的字符串('/users/:id'),自己拼 /users/${id} 不会剥 id
  • GET / DELETE / OPTIONS 的 variables → params,其余 → data
  • 可在 fetcher 上写默认 params / data
  • transform:(json, variables) => TData

无限列表:

const listItems = createFetcher<Item[], { keyword?: string }, number>('/items', {
  params: { pageSize: 20 },
  mapPageParam: (page) => ({ params: { page } }),
});

合并顺序(后者覆盖):options.params/data → meta.reqOpt → variables → mapPageParam。

单次覆盖走 TanStack meta.reqOpt,HTTP metadata 挂在 reqOpt.metadata:

runMutation(createUser, { name: 'sai' }, {
  reqOpt: { timeout: 5000, metadata: { traceId: 'x' } },
});

mutationFn(variables) 可不传第二参。

WebSocket

消息约定:{ event, data } JSON;心跳为文本 ping / pong。

import { WebSocketService } from '@hootool/http-client/websocket';

type Events = {
  chat: { text: string };
  online: { count: number };
};

const ws = new WebSocketService<Events>({
  pingInterval: 15_000,
  serverTimeout: 8_000,
  maxReconnectAttempts: 8,
  maxReconnectDelay: 15_000,
});

ws.connect('wss://example.com/ws');
ws.subscribe('chat', (msg) => console.log(msg.text));
await ws.send({ event: 'chat', data: { text: 'hi' } });
ws.disconnect(); // 会清掉排队中的重连

React:

import { useWebSocket, createUseWebSocket } from '@hootool/http-client/use-web-socket';

const { sendMessage } = useWebSocket(ws, 'chat', (msg) => {
  console.log(msg.text);
});

const useAppWS = createUseWebSocket(ws);
useAppWS('chat', (msg) => console.log(msg));

onMessage 换引用不会反复订阅。

License

MIT