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

@pluve/fetch2

v0.0.19

Published

fetch abort

Readme

@pluve/fetch2

基于 fetch 封装,保留与@pluve/fetch基本一致的用法。若用到文件上传,请参考@pluve/file-utility或者@pluve/storage-client

安装

pnpm add @pluve/fetch2

引用

import FetchAgent from '@pluve/fetch2';

使用说明

推荐优先使用以下三条主路径:

  • request/get/post:默认全局 runtime(兼容旧用法)
  • createRuntime:实例化 runtime(隔离配置,适合多应用/SSR)
  • createClient:按 baseURL 构建请求参数

快速开始(推荐)

import { createRuntime, RequestMethodsEnum } from '@pluve/fetch2';

const runtime = createRuntime({
  baseURL: 'https://api.example.com',
  requestSource: 'web-app',
  timeout: 15000,
});

runtime.setupReqInterceptor((req) => {
  req.headers = { ...(req.headers || {}), Authorization: 'Bearer token' };
  return false;
});

const data = await runtime.request({
  url: '/user/profile',
  method: RequestMethodsEnum.GET,
  retry: { enabled: true, retries: 2 },
});

本次优化概览(性能与鲁棒性)

  • 重试稳定性:为每次重试重新创建 AbortControllersignal,避免前一次中止影响后续尝试(此前会出现重试立即失败的问题)。
  • GET 查询参数升级:支持嵌套对象与数组的编码策略(dot/bracket/json),并提供最大递归深度限制,兼顾易读与兼容后端解析。
  • 文本与流处理更清晰:在响应处理阶段统一基于 Content-Type 选择解析方式,并保留 responseType 强制覆盖;文本响应支持流式增量读取与可选自动 JSON 解析。
  • 异常信息更详尽:对 4xx/5xx 响应提取 Retry-After(秒或 HTTP-Date),写入异常 data 前缀,便于重试策略准确参考。
  • 代码与导入小清理:移除未使用的解析函数导入,保持打包与运行更干净。

GET 查询参数(query)增强

在 GET 请求中,新增可选的查询编码策略与最大深度:

await FetchAgent.get({
  url: '/api/search',
  method: RequestMethodsEnum.GET,
  query: { k: '中文 空格', filters: { price: { min: 10, max: 100 }, tags: ['a','b'] } },
  queryEncodeStrategy: 'bracket', // 'dot' | 'bracket' | 'json'
  queryEncodeMaxDepth: 4,
});
// 'bracket' 示例:/api/search?k=%E4%B8%AD%E6%96%87%20%E7%A9%BA%E6%A0%BC&filters[price][min]=10&filters[price][max]=100&filters[tags][]=a&filters[tags][]=b

queryEncodeStrategyjson 或超过 queryEncodeMaxDepth 时,复杂对象会被 JSON.stringify 后作为单字段值,方便后端统一解析。

更多示例:

// dot 策略:以点号展开嵌套结构
await FetchAgent.get({
  url: '/api/search',
  method: RequestMethodsEnum.GET,
  query: { filters: { price: { min: 10, max: 100 }, tags: ['a','b'] } },
  queryEncodeStrategy: 'dot',
});
// 实际请求:/api/search?filters.price.min=10&filters.price.max=100&filters.tags=a&filters.tags=b

// json 策略:原样字符串化复杂对象,后端统一按 JSON 解析
await FetchAgent.get({
  url: '/api/search',
  method: RequestMethodsEnum.GET,
  query: { filters: { price: { min: 10, max: 100 }, tags: ['a','b'] } },
  queryEncodeStrategy: 'json',
});
// 实际请求:/api/search?filters=%7B%22price%22%3A%7B%22min%22%3A10%2C%22max%22%3A100%7D%2C%22tags%22%3A%5B%22a%22%2C%22b%22%5D%7D

GET 查询参数(query)

在 GET 请求中,使用 requestInfo.query 显式传递查询参数,内部通过 URLSearchParams 进行编码:

await FetchAgent.get({ url: '/api/list', method: RequestMethodsEnum.GET, query: { q: '中文 空格', tags: ['a','b'] } });
// 实际请求:/api/list?q=%E4%B8%AD%E6%96%87%20%E7%A9%BA%E6%A0%BC&tags=a&tags=b

注意:null/undefined 会被忽略;数组将作为多个同名键编码。

响应解析(responseType)与异常结构统一

支持通过 requestInfo.responseType 强制响应解析方式,取值:json | text | blob | arrayBuffer | ndjson | sse。默认仍基于 Content-Type 自动解析。

当使用 responseType: 'ndjson' 时,库会按行解析响应文本的每一行:

  • 空行跳过;
  • 可被 JSON.parse 的行解析为对象;
  • 非法 JSON 行会以原始字符串返回,避免整体失败;

你还可以通过 transformResponse(data, response, request) 在成功响应后对数据做二次转换或映射:

await FetchAgent.get({
  url: '/stream',
  responseType: 'ndjson',
  transformResponse: (data) => data.filter(Boolean),
  // 流式逐行
  onNdjsonLine: (item, raw) => {
    // item: JSON对象或原始字符串;raw: 原始行文本
    console.log('line:', item);
  },
});

当响应头为 Content-Type: text/event-stream(或显式指定 responseType: 'sse')时,将按 SSE 协议逐事件解析:

  • 事件块以空行分隔;
  • 识别 event:data:id: 字段;
  • data:"..." 为 JSON 字符串,库会自动尝试 JSON.parse;否则按原始字符串返回;
await FetchAgent.get({
  url: '/sse/stream',
  method: RequestMethodsEnum.GET,
  // 可省略,若服务端返回 text/event-stream 会自动启用
  // responseType: 'sse',
  onSseEvent: (evt) => {
    // evt: { event: string; data: any; id: string | null; raw: string }
    if (evt.event === 'reply' && evt.data?.payload) {
      console.log('reply content:', evt.data.payload.content);
    }
  },
});

非 2xx 响应将返回统一异常结构 IExceptionInfo,并包含结构化 details 字段(如 statusretryAfterMsisRetryable)。为兼容旧逻辑,data 仍保留字符串格式(如 status=<code>;...)。

同时,文本响应也支持流式读取:

await FetchAgent.get({
  url: '/large-text',
  responseType: 'text',
  onTextChunk: (chunk) => {
    // 每次接收到一段文本块
    console.log('chunk:', chunk.length);
  },
});

SSE 流式响应示例:

await FetchAgent.get({
  url: '/sse/stream',
  method: RequestMethodsEnum.GET,
  onSseEvent: (evt) => {
    // 每次接收到一个事件块触发一次
    console.log('event:', evt.event, 'id:', evt.id);
  },
});

可选重试机制(指数退避 + 抖动)

仅对幂等方法(GET/HEAD/OPTIONS)支持重试。可通过 retry 字段开启:

  await FetchAgent.get({
  url: '/api/list',
  method: RequestMethodsEnum.GET,
  retry: {
    enabled: true,
    retries: 3,           // 最大重试次数(不含首次)
    baseDelayMs: 200,     // 基准退避毫秒
    maxDelayMs: 2000,     // 最大退避毫秒
    jitter: 'full',       // 抖动策略:none/full
    retryOn5xx: true,     // 5xx 响应参与重试
    retryOnNetworkError: true, // 网络错误/超时参与重试
    retryOn429: true,     // 429(Too Many Requests)参与重试
    // methods: [RequestMethodsEnum.GET], // 可选,默认使用幂等方法集
  },
});

重试触发条件:

  • 网络错误 / 超时(异常码 10000/10001/10002)
  • HTTP 5xx(异常码为 RESPONSE_OTHER,且 data 包含 status=5xx;
  • HTTP 429(异常码通常为 RESPONSE_400_PLUSdata 包含 status=429;

非幂等方法(如 POST/PUT/PATCH/DELETE)默认不启用重试,除非明确指定 methods 并了解风险。

对于 429 与 5xx 响应,若开启对应重试(retryOn429/retryOn5xx),将优先使用 Retry-After 头来计算等待时间(支持秒数字符串与 HTTP-Date),并受 maxDelayMs 上限约束;若无该头或无法解析,则回退至指数退避策略。

当响应头包含 Retry-After 时,重试等待时间优先采用该值,受 maxDelayMs 上限约束(支持秒与 HTTP-Date):

  • 若为秒数字符串(如 Retry-After: 3),直接按秒数等待;
  • 若为 HTTP-Date(RFC 7231 格式,如 Wed, 21 Oct 2015 07:28:00 GMT),将转换为与当前时间的差值(秒)后等待; 若未提供则回退为指数退避策略。

示例:服务端返回 429/503 且 Retry-After 为未来日期,客户端等待约 1s 后重试。

重试的稳定性修复:每次重试都会创建新的 AbortControllersignal,避免上一次因 abort() 或超时导致后续尝试立即失败的情况。

导出方式

提供具名导出,亦保留默认导出,方便按需引入:

import { request, get, post, createRuntime, abortPendingRequestByKey } from '@pluve/fetch2';
import FetchAgent from '@pluve/fetch2';

ESM 导入示例

当前包默认发布 ESM(exports.import),推荐使用 ESM 导入:

// ESM(现代前端/Node 环境)
import FetchAgent, { request, get, post, createRuntime, RequestMethodsEnum } from '@pluve/fetch2';

// 类型(TypeScript)
// 自动从 package.json 的 types 字段解析,无需额外配置

若你的项目仍为 CJS,请通过构建工具转译后再消费本包,不建议直接 require('@pluve/fetch2')

类型说明

IRequestParams

interface RequestInit {
  /** A BodyInit object or null to set request's body. */
  body?: BodyInit | null;
  /** A string indicating how the request will interact with the browser's cache to set request's cache. */
  cache?: RequestCache;
  /** A string indicating whether credentials will be sent with the request always, never, or only when sent to a same-origin URL. Sets request's credentials. */
  credentials?: RequestCredentials;
  /** A Headers object, an object literal, or an array of two-item arrays to set request's headers. */
  headers?: HeadersInit;
  /** A cryptographic hash of the resource to be fetched by request. Sets request's integrity. */
  integrity?: string;
  /** A boolean to set request's keepalive. */
  keepalive?: boolean;
  /** A string to set request's method. */
  method?: string;
  /** A string to indicate whether the request will use CORS, or will be restricted to same-origin URLs. Sets request's mode. */
  mode?: RequestMode;
  /** A string indicating whether request follows redirects, results in an error upon encountering a redirect, or returns the redirect (in an opaque fashion). Sets request's redirect. */
  redirect?: RequestRedirect;
  /** A string whose value is a same-origin URL, "about:client", or the empty string, to set request's referrer. */
  referrer?: string;
  /** A referrer policy to set request's referrerPolicy. */
  referrerPolicy?: ReferrerPolicy;
  /** An AbortSignal to set request's signal. */
  signal?: AbortSignal | null;
  /** Can only be null. Used to disassociate request from any Window. */
  window?: null;
}
export interface IRequestParams extends RequestInit {
  key?: string; // 接口请求标识,用于取消请求
  url: string; // 接口请求链接
  method?: RequestMethodsEnum; // 请求方式
  headers?: ICustomHeader; // 接口请求头,可与全局的请求头合并
  useGlobalHeader?: boolean; // 是否使用全局请求头
  body?: any; // 请求体
  query?: Record<string, any>; // GET 查询参数对象(支持嵌套对象/数组)
  queryEncodeStrategy?: 'dot' | 'bracket' | 'json'; // GET 查询编码策略
  queryEncodeMaxDepth?: number; // GET 查询编码最大递归深度(超出后按 json 处理)
  submitDataType?: 'json' | 'form'; // 数据提交类型,支持application/json 和  application/x-www-form-urlencoded
  timeout?: number; // 接口超时时间,单位毫秒,默认15000
  timeStamp?: number; // 接口发起时的时间戳
  abortController?: AbortController; // 取消请求的控制器
  reqInterceptor?: IRequestInterceptor; // 请求拦截器
  respInterceptor?: IResponseInterceptor; // 响应拦截器
  [key: string]: any; // 支持携带自定义请求参数,可在拦截器中使用
}
Response
interface Body {
  readonly body: ReadableStream<Uint8Array> | null;
  readonly bodyUsed: boolean;
  arrayBuffer(): Promise<ArrayBuffer>;
  blob(): Promise<Blob>;
  formData(): Promise<FormData>;
  json(): Promise<any>;
  text(): Promise<string>;
}
interface Response extends Body {
  readonly headers: Headers;
  readonly ok: boolean;
  readonly redirected: boolean;
  readonly status: number;
  readonly statusText: string;
  readonly type: ResponseType;
  readonly url: string;
  clone(): Response;
}

IExceptionInfo

export interface IExceptionInfo {
  code: number; // EXCEPTION_KEYS
  message: string;
  data?: string | number;
  details?: {
    status?: number;
    retryAfterMs?: number;
    responseType?: string;
    responseSize?: number;
    isRetryable?: boolean;
    reason?: string;
  };
}
const EXCEPTION_KEYS = {
  ABORT_EXCEPTION_KEY: 10000, // 请求终止
  TIMEOUT_EXCEPTION_KEY: 10001, // 请求超时
  NETWORK_ERROR_EXCEPTION_KEY: 10002, // 请求网络异常
  INTERCEPT_AT_GLOBAL_LEVEL: 10003, // 被全局拦截器拦截
  INTERCEPT_AT_API_LEVEL: 10004, // 被接口拦截器拦截
  REQUEST_REDIRECT: 10005, // 请求重定向
  RESPONSE_400_PLUS: 10006, // 400相关响应
  RESPONSE_OTHER: 10007, // 其他异常
  REQUEST_CROSS: 10008, // 请求跨域
  RESPONSE_TIME_TRIGGER_THRESHOLD: 10009, // 接口耗时触发阀值
  BUSINESS_EXCEPTION: 10010, // 业务异常(由业务拦截器识别)
};

接口

setDebug

设置是否为 debug 模式。debug 模式下,控制台会打印详细的接口请求及响应信息,默认为关闭,可在开发测试模式下开启,建议在生产环境关闭。

FetchAgent.setDebug(true);

setupTimeout

设置超时时间,默认为 15s,单位为毫秒

FetchAgent.setTimeout(15000);

setupApiTimeCostThreshold

设置接口响应时长阀值,默认 1000ms,超过该值则会在控制台打印接口响应时长

FetchAgent.setupApiTimeCostThreshold(1000);

setupDefaultGlobalHeaderGenerator

设置统一请求头信息,若某个接口请求头有特殊要求,可在 GET、POST 方法中设置

FetchAgent.setupDefaultGlobalHeaderGenerator(() => ({
  'Content-Type': 'application/json',
  'X-Requested-With': 'XMLHttpRequest',
}));

setupDefaultCommonReqBodyGenerator

在请求体中设置通用参数,如鉴权信息等。若表单提交,则会忽略该方法设置的参数。

FetchAgent.setupDefaultCommonReqBodyGenerator(() => ({
  openx_header: 'xxx',
}));

setupReqInterceptor

接口请求全局拦截器,若返回 true,则终止请求。接口级别的拦截器优先级高于全局拦截器。

FetchAgent.setupReqInterceptor((req: IRequestParams) => false);

setupRespInterceptor

响应结果全局拦截器,若返回 true,则终止后续流程。可用于 token 过期、异常处理等场景。接口级别的拦截器优先级高于全局拦截器。此处读取 response 响应数据应先 clone,否在 return false 后会导致响应体重复使用的异常。

FetchAgent.setupRespInterceptor((requestParams: IRequestParams, response: Response) => false);

setupBusinessErrorInterceptor

设置业务异常统一拦截器。该拦截器在成功响应(HTTP 2xx)并完成解析/转换后执行:
返回 IExceptionInfo 表示识别到业务异常,将统一进入异常处理链(reject + setupOnError)。
返回 false/null/undefined 表示通过。

FetchAgent.setupBusinessErrorInterceptor((data, response, requestParams) => {
  // 示例:后端约定 success=false 表示业务失败
  if (data?.success === false) {
    return {
      code: 10010,
      message: data.message || '业务异常',
      data: data.message || '',
      details: { reason: 'business_failed' },
    };
  }
  return false;
});

setupOnError

设置全局错误处理函数 接口响应码非 200 时会回调,统一异常结构通过 IExceptionInfo 返回;
接口响应超时时会回调;
接口响应时长超过警告阀值时会回调;
可在异常日志上报场景下使用。

FetchAgent.setupOnError((err: IExceptionInfo, requestInfo: IRequestParams) => {
  // 统一异常结构
  // err.code 参见 EXCEPTION_KEYS
  // err.message 为预定义文案
  // err.data 对于 HTTP 非 2xx 响应将包含 "status=<code>;" 以及服务端返回内容(JSON 字符串或文本),二进制响应则记录类型与大小
  console.log(err, requestInfo);
});

sendRequest

发送请求
① 返回 Promise;若请求被拦截,则返回 rejected Promise,异常结构见 IExceptionInfo
② 支持 GET、POST、PUT、DELETE、PATCH 等请求方式。
③ 支持请求头、请求体、请求参数、请求超时、请求取消等功能。
④ 支持传入泛型,返回指定类型的数据。

/**
 * 发送请求
 * @param requestInfo
 */
export const request = <T = unknown>(requestInfo: IRequestParams): Promise<T> => {
  const requestParams = buildRequestOptions(requestInfo);
  if (requestParams.reqInterceptor && requestParams.reqInterceptor(requestParams)) {
    return Promise.reject(ExceptionUtil.generateExceptionByKey(ExceptionUtil.EXCEPTION_KEYS.INTERCEPT_AT_API_LEVEL));
  }
  if (handlerBeforeRequest(requestParams)) {
    return Promise.reject(ExceptionUtil.generateExceptionByKey(ExceptionUtil.EXCEPTION_KEYS.INTERCEPT_AT_GLOBAL_LEVEL));
  }
  requestMap.set(requestParams.key, requestParams);
  const apiPromise = doRequest(requestParams);
  return handlerApiResponse<T>(requestParams, apiPromise);
};

// 业务服务声明
export const fetchArticle = ({ pageNo, pageSize }: { pageNo: number; pageSize: number }) =>
  FetchAgent.sendPost<ApiCommonResponse<IArticleDataItem[]>>({
    url: articleService,
    body: {
      article: { articleType: null, keyword: null },
      pageNo,
      pageSize,
    },
    respInterceptor: async (requestParams, response) => {
      console.log('respInterceptor: ', requestParams);
      // 一定要这样写
      // console.log(response.clone().json());
      // 这样写报错
      // console.log(response.json());
      return false;
    },
  });

// 业务调用
try {
  const articleListResp = await fetchArticle({ pageNo: this.pageNo, pageSize: this.pageSize });
  this.updateArticles(refresh, articleListResp);
} catch (e) {
  this.showError();
}

为了兼容 @pluve/fetch,保留以下别名方法(建议优先使用 request/get/post): | 编号 | 方法名 | 说明 | | -- | -- | -- | | 1 | createRuntime | 创建隔离 runtime(推荐) | | 2 | request / sendRequest | 发送接口请求 | | 3 | get / post | 常用 GET/POST 请求 | | 4 | sendRequestCustomer / sendRequestPure | 兼容别名,建议迁移 | | 5 | sendGet / sendPost | 兼容别名,建议迁移 |

abortPendingRequestByKey

根据 key 终止请求

export const abortPendingRequestByKey = (key: string) => {
  if (!key) {
    return;
  }
  const requestItem = requestMap.get(key) as IRequestParams;
  if (requestItem && requestItem.abortController) {
    requestAbortManualFlag.set(key, true);
    requestItem.abortController.abort();
    requestMap.delete(key);
  }
};
FetchAgent.abortPendingRequestByKey('key');

clearRequest

清除 pending 中的请求

export const clearRequest = () => {
  // 只要还在此map中的请求,均为pending状态
  requestMap.forEach((requestItem) => {
    if (requestItem.abortController) {
      const key = requestItem.key || '';
      requestAbortManualFlag.set(key, true);
      requestItem.abortController.abort();
    }
  });
  requestMap.clear();
};
FetchAgent.clearRequest();

外部依赖

若低版本浏览器需要依赖 whatwg-fetch

issues 说明

暂无版本计划