配了多年 fetch abort,今天才发现取消的 reason 在 Response 层面从来没传过来——Chrome 154 把这件事彻底修好了

痛点故事

你写了一个搜索组件:用户每次按键都发一个新请求,上一个请求用 AbortController.abort('user typed') 取消。取消时传了 reason = 'user typed newer request'。

问题是:下游代码拿到被 abort 的 Response 后,读 response.body 得到的是一个已取消的 ReadableStream,catch 块里只有 AbortError 字符串——你亲手传的那个 reason 去哪了?

答案是:之前根本传不过去。reason 只在 signal.reason 上有,Response 层面完全感知不到。Chrome 154 把这件事修掉了。


旧行为(Chrome 153 及以前)

const controller = new AbortController();

// 下游代码拿到 response 和 signal
async function fetchData(signal) {
  const response = await fetch('/api/search', { signal });
  return response;
}

// 调用方
controller.abort('user navigated away');

try {
  const resp = await fetchData(controller.signal);
  // resp 本身不知道被 abort 了
  // signal.reason === 'user navigated away'
  // 但 resp.body / resp.json() 层面没有这个 reason
  const data = await resp.json(); // 可能抛出异常,但 reason 信息丢失
} catch (e) {
  // e.name === 'AbortError'
  // e.message === 'The user aborted a request.'
  // signal.reason === 'user navigated away' — 只有 signal 上有
  // Response 层面完全不知道原始 reason
}

问题总结:

  • response.body cancel 后,reason 信息只存在于 signal.reason
  • 中间件/拦截器层如果只持有 response,读不到 reason
  • 无法区分「用户取消」「超时」「主动放弃」等不同 abort 场景

新行为(Chrome 154)

Chrome 154 将 abort reason 传递到 Response 对象及其 ReadableStream 的方法中。现在你在 response.json() 的 reject 中也能拿到原始 reason:

const controller = new AbortController();
const reason = new Error('timeout');

async function fetchData(signal) {
  const response = await fetch('/api/search', { signal });
  // Chrome 154: body 方法层面也能感知到 abort reason
  return response;
}

controller.abort(reason);

try {
  const resp = await fetchData(controller.signal);
  // 之前: response.body 方法层面读不到 reason
  // 现在: body 方法会传播 reason
  const data = await resp.json();
} catch (e) {
  // Chrome 154: e.cause === reason (原始 Error 对象)
  // 可以通过 e.cause 判断具体的 abort 原因
  if (e.cause === reason) {
    console.log('This fetch was aborted due to timeout');
  }
}

核心变化:

  • AbortController.abort(reason) 的 reason 通过 response.body 的 ReadableStream 传播
  • catch 块中 error.cause 即为调用方传入的原始 reason 对象
  • signal.reason 和 body 方法现在都能拿到同一个 reason
  • 规范对齐:WHATWG Fetch 规范一直要求这样,Chrome 之前实现不完整

实际应用场景

1. 搜索防抖:区分取消原因

class SearchService {
  constructor() {
    this.controller = null;
  }

  async search(query) {
    // 取消上一个请求,附带原因
    if (this.controller) {
      this.controller.abort('superseded by newer query');
    }
    this.controller = new AbortController();
    const reason = { type: 'search', query, startTime: Date.now() };

    try {
      const resp = await fetch(`/api/search?q=${query}`, {
        signal: this.controller.signal
      });
      const data = await resp.json();
      return data;
    } catch (e) {
      // Chrome 154: 通过 e.cause 判断是否为超时
      if (e.cause?.type === 'timeout') {
        console.log('Search timed out for:', query);
      }
      // 判断是否为被新请求覆盖
      if (e.cause?.type === 'superseded') {
        // 忽略,这是正常的防抖取消
        return null;
      }
      throw e;
    }
  }

  timeout(ms) {
    if (this.controller) {
      this.controller.abort({ type: 'timeout', ms });
    }
  }
}

2. 中间件层 reason 传递

// 之前:中间件只知道 signal 被 abort,不知道具体原因
// 现在:中间件可以从 body error 的 cause 中拿到原始 reason

async function authMiddleware(request, fetch) {
  const response = await fetch(request);
  if (!response.ok) {
    return response;
  }
  return response;
}

// 包装 fetch,自动传播 reason
const smartFetch = (signal) => async (input, init = {}) => {
  const response = await fetch(input, { ...init, signal });
  // 将 signal.reason 附加到 response
  // Chrome 154 这步由浏览器自动处理
  return response;
};

技术细节

规范来源: WHATWG Fetch 规范要求 abort reason 应该传播到 Response 及其 body 消费者。Chrome 154 之前只实现了「promise reject 时 reason 在 signal 上」,但 body 的 ReadableStream 取消时 reason 没有正确传播。

影响范围: 所有使用 fetch() + AbortController 并且在 abort 后继续读取 response.body 的代码。

兼容性: Chrome 154+(2026-09 稳定版)。Firefox 尚不支持(Firefox 158 无相关记录)。Safari 未知。这是规范对齐修复,不是新 API,无需特性检测——行为改变是向规范靠拢。

与其他 AbortController 特性的关系:

  • AbortSignal.timeout() — 超时用的 signal.abort
  • AbortSignal.any() — 组合多个 signal
  • 本次修复是这些场景下 reason 传递的底层保障

结论

AbortController.abort(reason) 传的是什么,现在整个 fetch 链路都能看到了:signal 上有,Response.body 层面也有。这是 Chrome 154 对 WHATWG Fetch 规范的完整实现——之前只做了一半。

如果你在中间件、service layer 或流处理代码中处理被取消的 fetch,现在可以准确知道取消原因,而不只是「abort error」。这让 abort 错误处理从二元(取消/未取消)变成多元(超时/用户取消/新请求覆盖/组件卸载),调试和业务逻辑都会清晰很多。


下一步: 检查你的项目中是否有 fetch(...).catch(e => { /* 只判断 e.name === 'AbortError' */ }) 的代码——它们现在可以通过 e.cause 拿到具体 reason 了。

评论区

0 条评论

登录后可评论。

阿监·前端工程化 141 阅读