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

@sgysldz/http-request

v1.0.0

Published

企业级 axios 封装:拦截器、错误处理、取消、重试、Token 管理

Readme

http-request

基于 axios传输层封装(企业常见 HTTP 能力的基础设施),在不大改 axios 使用方式的前提下,补齐:

| 能力 | 摘要 | | -------------- | --------------------------------------------------------------------------------------------- | | Trace Id | 对每个请求自动补齐 trace 请求头(可按请求关闭或覆盖) | | 取消与竞态 | 通过 cancelKey 让「同语义」请求只保留最新一次,旧请求自动 abort | | 在途去重 | 对幂等请求(默认 GET / HEAD / OPTIONS)合并尚未结束的相同请求,共享同一个 Promise | | 重试 | 可配置次数、是否重试、退避;默认偏保守,避免 POST 等误重试 | | 离线短路 | 在支持 navigator 的环境里,离线时尽早失败,避免无意义等待 | | 错误归一 | 将 axios / 取消 / 未知抛错统一为 HttpError,便于类型守卫与上报 |

本包刻意不做:鉴权、Authorization / Token 刷新、业务 { code, data } 协议解包、根据 HTTP 401 跳转登录、全局 Toast/MessageBox。这些应在业务项目里通过 axios 拦截器再包一层 API 模块实现,以便与你们的账号体系、路由、UI 框架一致。


目录


设计边界与分层建议

推荐的分层是:

  1. 本包:网络传输语义(取消、去重、重试、trace、错误形状)。
  2. 业务 axios 拦截器:请求侧注入 Token / 租户头;响应侧解析后端业务信封(若有)、统一错误或刷新 token。
  3. API 函数层:按接口路径组织 getUser(id) 等,对外返回业务 DTO。

这样 http-request 可以稳定复用在多个项目,而业务规则只写一次在你自己的封装里。


安装与引用

Monorepo 内其他包在 package.json 中声明依赖(示例):"http-request": "workspace:*",然后:

import { createHttpClient, HttpError, isHttpError, IDEMPOTENT_METHODS } from "http-request";

对外发布时,本包 不会把 axios打进 bundletsdown 中将 axios 标为 external),宿主项目仍需安装 axios,版本建议与该 monorepo 的 pnpm catalog 保持一致。

源码开发时,package.json"main" / "exports" 可能指向 ./src/index.ts,由宿主工具链解析;发布后 publishConfig 指向 dist/**


创建客户端 createHttpClient

import { createHttpClient } from "http-request";

const http = createHttpClient({
  baseURL: "/api",
  timeout: 15_000,
  headers: { "X-Client": "web" },
  retry: 2,
  retryDelay: 300,
  dedupe: true,
  traceIdHeader: "X-Trace-Id",
  onTransportError: (err) => console.error("[transport]", err.type, err.message),
  debug: false,
});

type User = { id: string; name: string };
const list = await http.get<User[]>("/users");

返回值类型为 HttpClient:在 raw 未为 true 时,get/post 等方法返回 Promise<T>(即 response.data,与 axios 常见用法一致;需要 headers / status 时使用 { raw: true }

实例上额外挂载:

  • http.cancel(key: string):按 key 中止当前未完成请求;
  • http.cancelAll():取消池内全部请求并清空去重池(见下文)。

工厂选项 CreateHttpClientOptions(默认值)

| 选项 | 类型 | 默认 | 说明 | | ------------------ | ------------------------ | ------------------------------ | ------------------------------------------------------------------- | | baseURL | string | 无 | 传给 axios.create。 | | timeout | number | 15000 | 毫秒;超时归一为 HttpErrortype: "timeout"。 | | headers | Record<string, string> | 无 | 实例级默认头;单次请求仍可覆盖。 | | retry | number | 0 | 实例默认不重试;可被单次请求的 config.retry 覆盖。 | | retryDelay | number | 300 | 重试基础间隔(毫秒),见 退避公式。 | | retryCondition | RetryCondition | 源码内默认值 | (error, nextAttemptNumber) => boolean。 | | retryBackoff | RetryBackoff | 指数退避 | 单次等待上限内部硬编码为 10000 ms。 | | dedupe | boolean | true | false 时关闭整实例的在途 GET 去重包装。 | | traceIdHeader | string | "X-Trace-Id" | 设为 "" 可关闭自动 trace。 | | genTraceId | () => string | randomUUID 去横线 / 32位 hex | 自定义 trace 生成。 | | onTransportError | (HttpError) => void | 无 | 见 专节。 | | debug | boolean | false | 打印请求/响应行级日志,不含 body。 |


请求级扩展字段 RequestExtraConfig

这些字段挂在标准 axios config 上;axios 本身忽略它们,仅本库拦截器读取。

| 字段 | 说明 | | ---------------- | --------------------------------------------------------------------------------------------- | | cancelKey | 字符串。同 key 的新请求会 abort 尚未完成的旧请求;结束或失败(非 cancel)时从取消池释放。 | | dedupe | false 时本条请求不参与在途去重(仅当工厂未关闭去重时有效)。 | | retry | 覆盖工厂默认重试次数。 | | retryDelay | 覆盖工厂默认基础延迟。 | | retryCondition | 覆盖工厂默认判别函数。 | | retryBackoff | 覆盖工厂默认退避。 | | raw | true 时返回完整 AxiosResponse;否则返回 response.data。 | | silent | true 时失败仍 reject,但不会触发 onTransportError。 | | meta | 任意 JSON 友好的对象信息;暴露在 HttpError.toJSON() 中便于上报附加字段。 |

示例

await http.get("/search", {
  params: { q: "vue" },
  cancelKey: "search-list",
  meta: { scene: "list", tab: "all" },
});

const res = await http.get("/health", { raw: true });
console.log(res.status, res.headers);

http.cancel("search-list");
http.cancelAll();

拦截器与内部流程

用一张简化的因果关系说明「一次调用」经过哪些步骤(不涉及你在业务里额外注册的拦截器先后顺序):

  1. 可选:GET 去重包装 若工厂开启 dedupe,且本条满足 去重条件,则 instance.request 会先查表里是否已有未结束的同 key Promise;若有,直接返回该 Promise(不再走入下一步的新请求)。
  2. 请求拦截器(本库)
    • 离线:若在浏览器环境里 navigator.onLine === false直接抛出 HttpErroroffline),不发起 HTTP。
    • Trace:若配置了 traceIdHeader 且请求头尚无该键,则写入新 id。
    • cancelKey:若存在 cancelKeyconfig.signal 未预设,则通过 CancelManager 绑定 AbortController。若你自带 signal,本库不会覆盖,方便与外层 AbortController 组合。
  3. axios 发起真实网络请求。
  4. 响应拦截器(本库)
    • 成功:释放 cancelKey、按 raw 决定返回 AxiosResponsedata
    • 失败:将错误归一为 HttpError;若非「取消」则释放 cancelKey;再根据重试策略 延迟后 再次调用 instance.request(replayConfig),或 reject

重试克隆配置时会 去掉上一轮 signal,避免沿用已失效的 Abort 状态。


取消:cancelKeycancel / cancelAll

CancelManager 行为(实例内部使用)

  • acquire(key):若该 key 上已有控制器,会先对旧控制器 abort(),再为本次请求放入新 AbortController
  • release(key, controller):请求完结时移除;若池中当前 controller 不是传入的这个(例如期间又来了一次 acquire),则不误删,避免释放错对象。
  • cancel(key):对当前 key abort 并从池移除。

http.cancel(key)http.cancelAll()

  • http.cancel(key):等价于对内部 CancelManager 做一次 cancel(key)
  • http.cancelAll():对所有追踪中的请求 abort,并 dedupePool.clear(),避免去重表中残留已不再需要的 Promise。

典型用法

  • 列表检索:筛选条件变化时对同一 cancelKey(如 "user-list")发新请求,旧请求自动作废。
  • 路由离开 / 注销登录:调用 cancelAll() 一次清场。

去重:dedupebuildDedupeKey

何时会去重(全部满足)

  1. 工厂 dedupe !== false
  2. 本条 dedupe !== false
  3. HTTP 方法属于 IDEMPOTENT_METHODSgetheadoptions
  4. 走包装后的 request 入口。

POST / PUT / DELETE 等不会去重,避免合并「本应两次提交」的请求。

buildDedupeKey 的规则

源码形式(概念上):

METHOD url|stableStringify(params)|stableStringify(data)

  • METHOD 会转成大写,url 为该项上的 config.url
  • stableStringify:对「普通对象」的键排序后再 JSON.stringify,使属性顺序不同的对象仍得到相同 key;无法序列化时退化为 String(value)undefined 视为空字符串段。

生命周期

  • 仅合并 in-flight:第一次请求未完成前,后来的同 key 调用共享同一 Promise
  • 任一 settle 后:该 key 从池移除;下一次相同参数会发起新一轮 HTTP。
  • 与取消:全局 cancelAll 会清空去重池,避免长时间持有引用。

导出 buildDedupeKeyDedupePool 可供你在不写 createHttpClient 的场景下自建合并逻辑。


重试:retry、默认策略与退避公式

参与计算的变量

  • maxRetryconfig.retry ?? 工厂.retry(工厂默认常为 0)。
  • 内部 state.attempt:从 0 开始累加;每进入一次「准备重试」前会 +1
  • retryCondition(err, attemptNumber) 的第二个参数:实现里传入的是 state.attempt + 1,即含义为「这一轮是不是第几次重试尝试(从 1 起计数)」。

仅当 state.attempt < maxRetryretryCondition 返回 true 时才会 sleepinstance.request(replayConfig)

默认 retryCondition 语义(简述)

源码逻辑可概括为:

| 分支 | 结果 | | --------------------------------------------- | ------------------------------------------------------ | | HttpError.type === "cancel" | 不重试 | | HTTP 方法不是 IDEMPOTENT_METHODS 之一 | 不重试(故默认 POST 等不会因网络失败自动重试) | | timeout 或错误路径体现的 offline | 重试 | | network 且无 response?.status | 重试(视为未拿到 HTTP 语义的差错) | | networkresponse.status500–599 | 重试 | | 其他(含 4xx) | 不重试 |

你可以为某次调用单独传入 retryCondition,在确认接口幂等的前提下让 POST 也能重试。

退避公式(默认实现)

对第 attempt 次重试前等待:

[ \text{delay} = \min\bigl(\text{baseDelay} \times 2^{(\text{attempt} - 1)},\ 10000\bigr) ]

其中 baseDelay = config.retryDelay ?? 工厂.retryDelay(默认 300 ms)。attempt 这里与内部 state.attempt 一致(第一次重试前 state.attempt 已加为 1,因此第一次等待通常是 baseDelay * 2^0)。

单次等待上限10000 ms(代码常量 DEFAULT_RETRY_MAX_DELAY)。


Trace Id

  • 默认响应头名为 X-Trace-Id
  • 若调用方已在 headers 设置同名键,库不会覆盖(便于接入已有网关链路)。
  • traceIdHeader 设为 "" 时整块逻辑跳过(不自动生成、不写头)。
  • 默认生成:globalThis.crypto.randomUUID() 去掉横线;不可用时降级为随机 32 位十六进制字符串。

错误模型 HttpError 与归一化规则

类型一览 HttpErrorType

| type | 典型含义 | | --------- | ----------------------------------------------------------------------------------------------- | | network | 多数 AxiosError 归为这一类;附带 responsecode(多为 HTTP status)。不代表一定断网。 | | timeout | axios 判定超时(源码以 ECONNABORTED 等为依据)。 | | offline | 请求拦截器抛出,或 axios 分支里检测到 navigator.onLine === false。 | | cancel | axios.isCancel 或 Abort 链路。 | | unknown | 非 AxiosError、非已有的 HttpError 的普通 Error;或其它值被转成字符串 |

说明

  • 4xx 在默认策略下通常为 network + code是否与 401 搭配登录跳转由业务自行实现,本库只提供常见 status 的中文 message 模板(便于直接展示或使用 toJSON 上报)。
  • 401/403 的文案仅是默认提示字符串,不包含「自动跳转」语义。

默认状态文案表(节选,完整见源码 STATUS_HINT):400 / 401 / 403 / 404 / 408 / 429 / 500 / 502 / 503 / 504 等。

HttpError 上有用的字段

  • typemessagename(固定为 HttpError
  • code:常为 HTTP status
  • config:最近一次相关请求配置(若可得)
  • response:axios response(若有)
  • cause:在非旧环境可能没有;支持时指向原始 AxiosError

toJSON()

返回普通对象:nametypecodemessageurlmethodstatusmeta(来自 config.meta)等字段,便于 JSON.stringify 发往日志/SDK。

类型守卫

import { isHttpError, isCanceledError, isNetworkError, isTimeoutError } from "http-request";

try {
  await http.get("/x");
} catch (e) {
  if (isCanceledError(e)) return;

  if (isTimeoutError(e)) {
    // 超时弱网提示等
  }

  if (isNetworkError(e) && e.code === 401) {
    // 业务:跳转登录、清 token —— 与本库无关,自行实现
  }

  if (isHttpError(e)) {
    console.log(e.toJSON());
  }
}

调试 debug

debug: true 时向控制台打印(不含 body):

  • 请求发出:→ METHOD url
  • 成功响应:← status METHOD url

请勿在生产环境长期开启(避免噪音与潜在的路径信息泄露)。


onTransportError 与全局提示

若在工厂传入 onTransportError

  • HttpError 已确定reject 之前,若非 cancel 且请求未设 silent: true,会调用 onTransportError(err)
  • 常用于「全局 toast:网络开小差了」等车;不负责路由跳转等业务副作用。

若在页面内自行 try/catch 并希望不要触发全局 toast,使用该请求的 silent: true


与业务拦截器组合

常见做法

  1. const http = createHttpClient(...) 后立即 http.interceptors.request.use,注入 Authorization、租户、语言等。
  2. 全局 onTransportError 弹出非侵入式提示;关键页面用 silent + 本地 catch 覆盖体验。

关于执行顺序

axios 对响应阶段的拦截器采用「后注册先执行」等链式规则;你在 createHttpClient 之后追加的拦截器与库内置拦截器的先后,会决定你拿到的是原始 AxiosError 还是已归一的 HttpError。若你需要完全掌控,请阅读当前使用的 axios 官方文档中 Interceptor 章节,并通过实验确认错误对象形态。稳妥做法:页面与模块级逻辑以 catch + isHttpError 为主,减少对响应拦截器的依赖。


构建与发布产物

pnpm --filter http-request build
  • tsdown 产出 dist:ESM、CJS 与 d.ts(见 tsdown.config.ts)。
  • axios不打进 bundle,deps.neverBundle 包含 axios
  • platform:配置为 browser(若以 Node 直接跑,请注意 navigator 等与运行环境是否兼容)。

类型与导出索引

值 / 类

| 导出 | 作用 | | --------------------------------------------------------------------------------------- | --------------------------------------------------------- | | createHttpClient | 工厂函数,返回带 cancel/cancelAllHttpClient | | CancelManager | 可按 key acquire/release/cancel 的取消管理器 | | DedupePool | 按 key 缓存在途 Promise,结束后清理 | | buildDedupeKey | 由 method/url/params/data 生成稳定 key | | HttpError | 归一错误类 | | IDEMPOTENT_METHODS | 默认幂等方法名列表(常量) | | isHttpError / isCanceledError / isNetworkError / isTimeoutError | 类型守卫 |

常用类型(export type

CreateHttpClientOptionsHttpClientHttpInternalRequestConfigHttpRequestConfigRequestExtraConfigRetryBackoffRetryConditionTraceIdGeneratorHttpErrorOptionsHttpErrorType 等——详情以 src/types.ts 为准。


常见问题(FAQ)

为什么在请求拦截器里就抛 offline,而不是发到服务器再报错? 尽早失败可缩短加载态与无用等待;是否与「假在线」环境下的策略冲突,可由业务评估(例如再配合你们自己的连通性探测)。

GET 去重会和 cancelKey 冲突吗? 二者解决不同问题:去重合并「完全相同」且在途的请求;cancelKey 取消「同 key」上的上一次未完成请求。建议对不同语义分支使用不同 cancelKey,避免本应分开的去重被撞在一起。

为什么默认不给 POST 重试? 重复提交可能产生重复订单、重复扣款等;默认只在幂等方法上自动重试。若业务确认安全,再开放 retry + 自定义 retryCondition

Node 环境没有 navigator 会怎样? 离线检测分支可能不触发;若在 Node 报错,可自行评估 polyfill / 不传 navigator 相关逻辑。平台仍以浏览器为主(见 tsdown platform)。

为何有时 cancelHttpError 没带 config 归一路径里对 axios.isCancel 的实现只保证 HttpError(..., message),可按需在业务侧用 cause(若运行时支持)追查。