DEBUG.WIKI
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
搬瓦工;先看线路,再选 VPS;CN2 GIA · 对应线路套餐;查看线路 →
V.PS:让项目靠近用户,亚太、欧洲、北美 KVM VPS

Got 14 自定义 handler 后 cancel 报私有字段 #state 错误

Got 14.6.6 的 handler 返回新 Promise 时,cancel 方法可能失去原接收者。迁移到受支持版本的 AbortSignal,分别检查异步准备和网络阶段的取消。

Got 14.6.6 的请求平时正常返回,一旦自定义 handler 返回了另一个 Promise,再调用请求上的 cancel(),却抛出:

TypeError: Cannot read private member #state from an object whose class did not declare it

这个组合要检查 handler 的返回对象。错误里的私有字段并不表示业务对象少定义了一个属性,也不应靠给 Promise 添加 #state 来处理。

新 Promise 改变了 cancel 的接收者

旧版 Got 允许 handler 包装 next(options) 的结果。若 handler 直接返回原来的请求 Promise,取消方法仍属于原对象;若先进入异步函数,返回值会变成另一个 Promise:

const handlers = [
  (options, next) => {
    if (options.isStream) return next(options);
    return (async () => {
      await prepareRequest();
      return next(options);
    })();
  },
];

这里的 prepareRequest 代表应用自己的请求准备函数,不是 Got 的内置方法。

14.6.6 会向包装后的对象复制部分请求属性,并直接执行 result.cancel = promise.cancel。随后通过 result 调用 cancel,方法接收到的是外层 Promise;它不是拥有内部私有状态的原对象,于是触发私有字段检查。是否真的发生网络传输,需要另外核对请求记录,不能从这个 TypeError 判断。

把业务代码改成 () => request.cancel(),仍然是在调用包装结果上的方法,不能恢复库内部已经丢失的接收者。捕获异常后当作取消成功,也会掩盖请求可能继续运行的事实。

向 AbortSignal 迁移,同时保留异步准备的取消检查

Got 14 已不再维护;Got 15 移除了这套 Promise.cancel 接口。当前正式 Got 16.1.0 接受 AbortSignal,并要求 Node.js 22 或更新版本。升级应连同运行时、ESM 加载方式、选项和 handler 行为一并检查,不能只替换锁文件后继续调用 cancel。

若异步准备只负责生成选项,可以先完成准备,再直接调用 Got,避免为了准备阶段重包请求对象。以下函数接收已按业务约定校验的目标地址;prepareOptions 是应用现有函数,返回认证、请求时限等选项:

import got from 'got';

export async function fetchPrepared(url, signal, prepareOptions) {
  signal.throwIfAborted();
  const options = await prepareOptions(signal);
  signal.throwIfAborted();
  return got(url, { ...options, signal });
}

调用方持有 AbortController,将 controller.signal 传进来;取消时调用 controller.abort(),不再从返回的 Promise 取 cancel。signal 放在展开选项之后,避免准备函数意外覆盖本次取消信号。

前后两次 throwIfAborted 分别挡住进入准备前和准备结束后的取消。准备函数本身若不消费 signal,中间的工作仍可能继续,不能声称取消会立即终止任意准备逻辑。已有 handler 还承担签名、流式响应或其他功能时,应逐项迁移,不能直接删去全部 handlers。

核对取消到达哪一层

在受控的只读慢响应入口分别验证:准备前取消、准备中取消、网络等待时取消,以及不取消的正常返回。前两种情况下,不应在准备完成后又发起请求;网络阶段要核对实际连接或服务端记录,而不只看外层 Promise 已拒绝。

保留原来的请求时限和重试规则。取消不会撤销服务端已经提交的写入,对结果不明的修改请求,仍需按业务身份查询状态,避免把一次取消异常变成重复提交。