如何检测网络请求异常 `observeNetworkError`

212 阅读5分钟

image.png

图片来自 bing.com

背景

想实现一个 Tampermonkey 插件,当发现某个接口报错则使用替代接口。比如某个 AI 网站接口挂了,我们想替换成 kimi 或 deepseek 的接口,但是我们没法修改这个网站的源码。

或者我们出于其他目的比如前端监控,看到开发者工具 Network 面板内接口报错 404 或 500,如何监听这类错误呢?

如:GET https://example.com/api/completion?query=3 500 (Internal Server Error)

如何实现?

PerformanceObserver to the Rescue 💉

我们可以使用 PerformanceObserver 这个观察器,过滤出 initiatorTypefetch 且状态码是我们想要监控的即可。

代码如下,大家可在 F12 或控制台执行,然后随意发出一个报错的 fetch 请求。

const observer = new PerformanceObserver((list) => {
  list.getEntries().forEach((entry) => {
    console.log('entry', entry)
    
    // @ts-expect-error `entry` 类型必定为 `PerformanceResourceTiming` https://github.com/microsoft/TypeScript/issues/58644
    if (entry.initiatorType === 'fetch' || entry.initiatorType === 'xmlhttprequest') {
      if (entry.responseStatus >= 400) {
        console.error('网络请求错误:', entry.name, '状态码:', entry.responseStatus);
      }
    }
  });
});

observer.observe({ type: 'resource', buffered: true });

代码说明

  1. 一般异常码都是 [400,500][400, 500]entry.responseStatus >= 400
  2. 注意entry.initiatorType 会报 TS 类型错误,因为 entry 的类型被认为是 PerformanceEntry 没有该字段,原因是 TS 无法做到如此精确的推导问题 github.com/microsoft/T… ,但实际上我们监听的是 typeresource 那么 entry 类型必定为 PerformanceResourceTiming 则自然有 initiatorType,故这里使用 @ts-expect-error 忽略即可。
classDiagram
    class PerformanceEntry {
        +String name
        +String entryType
        +DOMHighResTimeStamp startTime
        +DOMHighResTimeStamp duration
        +toJSON()
    }
    
    class PerformanceResourceTiming {
        +String initiatorType
        +DOMHighResTimeStamp fetchStart
        +DOMHighResTimeStamp domainLookupStart
        +DOMHighResTimeStamp domainLookupEnd
        +DOMHighResTimeStamp connectStart
        +DOMHighResTimeStamp connectEnd
        +DOMHighResTimeStamp secureConnectionStart
        +DOMHighResTimeStamp requestStart
        +DOMHighResTimeStamp responseStart
        +DOMHighResTimeStamp responseEnd
        +Number transferSize
        +Number encodedBodySize
        +Number decodedBodySize
        +String nextHopProtocol
        +String serverTiming
        +Number workerStart
        +Number redirectStart
        +Number redirectEnd
    }
    
    PerformanceEntry <|-- PerformanceResourceTiming
    
    note for PerformanceResourceTiming "resource timing API\n监控网络资源加载性能"
    note for PerformanceEntry "基础性能条目接口\n所有性能监控的基类"

使用示例

PerformanceResourceTiming 继承自 PerformanceEntry

const observer = new PerformanceObserver((list) => {
  list.getEntries().forEach((entry) => {
    // entry 是 PerformanceResourceTiming 实例
    // 既有父类 PerformanceEntry 的属性
    console.log('名称:', entry.name);
    console.log('类型:', entry.entryType); // 固定为 'resource'
    
    // 也有子类特有的属性
    console.log('发起类型:', entry.initiatorType); // script/img/css等
    console.log('DNS查询时间:', entry.domainLookupEnd - entry.domainLookupStart);
    console.log('TCP连接时间:', entry.connectEnd - entry.connectStart);
    console.log('响应时间:', entry.responseEnd - entry.responseStart);
  });
});

封装

我们封装下“检测网络请求异常”的函数。

首先我们封装一个对任何 resource 的监听器。

type IProps = {
  when: (entry: PerformanceResourceTiming) => boolean;
  callback: (entry: PerformanceResourceTiming) => unknown;
  buffered?: boolean;
}

function observeNetwork({ when, callback, buffered = true }: IProps) {
    const observer = new PerformanceObserver((list) => {
      list.getEntries().forEach((perfEntry) => {
        const entry = perfEntry as PerformanceResourceTiming;

        if (when(entry)) {
          callback(entry)
        }
      });
    });

    // `type resource` 可以监听这些类型的资源:
    // `"link","script","img","xmlhttprequest","fetch","beacon","css"`
    observer.observe({ type: 'resource', buffered });
    
    return () => {
      observer.disconnect()
    }
}
classDiagram
    class PerformanceEntry {
        <<abstract base class>>
        +String name
        +String entryType
        +DOMHighResTimeStamp startTime
        +DOMHighResTimeStamp duration
        +toJSON() Object
    }
    
    class PerformanceResourceTiming {
        <<重点:网络资源性能监控>>
        +String initiatorType
        +DOMHighResTimeStamp fetchStart
        +DOMHighResTimeStamp domainLookupStart
        +DOMHighResTimeStamp domainLookupEnd
        +DOMHighResTimeStamp connectStart
        +DOMHighResTimeStamp connectEnd
        +DOMHighResTimeStamp secureConnectionStart
        +DOMHighResTimeStamp requestStart
        +DOMHighResTimeStamp responseStart
        +DOMHighResTimeStamp responseEnd
        +Number transferSize
        +Number encodedBodySize
        +Number decodedBodySize
        +String nextHopProtocol
        +PerformanceServerTiming[] serverTiming
        +DOMHighResTimeStamp workerStart
        +DOMHighResTimeStamp redirectStart
        +DOMHighResTimeStamp redirectEnd
        +DOMHighResTimeStamp workerStart
        +DOMHighResTimeStamp redirectStart
        +DOMHighResTimeStamp redirectEnd
        +deliveryType() String
        +renderBlockingStatus() String
    }
    
    PerformanceEntry <|-- PerformanceResourceTiming
    
    %% 时间线可视化
    class ResourceTimingPhases {
        <<时间线阶段>>
        1. 重定向阶段
        2. DNS查询阶段
        3. TCP连接阶段
        4. TLS协商阶段
        5. 请求阶段
        6. 响应阶段
    }
    
    %% 资源类型分类
    class ResourceTypes {
        <<可监控的资源类型>>
        - script
        - link (CSS)
        - img
        - xmlhttprequest
        - fetch
        - iframe
        - audio/video
        - beacon
        - other
    }
    
    PerformanceResourceTiming --* ResourceTimingPhases : 包含
    PerformanceResourceTiming --* ResourceTypes : 分类依据

然后封装一个监听所有 API 请求且报错的:

function observeErrorRequest(onError: IProps['callback']) {
  const disconnect = observeNetwork({
    when: ({ initiatorType, responseStatus }) => {
      return responseStatus >= 400 && (initiatorType === 'fetch' || initiatorType === 'xmlhttprequest')
    },
    
    callback: (entry: PerformanceResourceTiming) => {
      onError(entry)
    }
  })
  
  return disconnect
}

用法就比较简单了:

observeErrorRequest((entry) => {
  console.error('网络请求错误:', entry.name, '状态码:', entry.responseStatus);
})

// 当不需要监听的时候
// disconnect()

代码解释

创建一个 PerformanceObserver 来监控资源加载性能指标,重点解释下 typebufferedinitiatorType 参数的作用:

type: 'resource'

  • 含义:指定要观察的性能条目类型。type resource 可以监听这些类型的资源 "link","script","img","xmlhttprequest","fetch","beacon","css"
  • 作用:这里设置为 'resource' 表示观察所有资源加载的性能数据
  • 其他常见类型,每一种类型都对应一个 interface PerformanceXxxTiming,更多见 PerformanceEntry: entryType property - MDN
    • 'largest-contentful-paint' - interface LargestContentfulPaint
    • 'navigation' - 页面导航性能 PerformanceNavigationTiming
    • 'paint' - 绘制性能(如 FP、FCP)。描述从 render tree 到绘画在屏幕上的一个个像素 PerformancePaintTiming
    • 'longtask' - 长任务,阻塞 UI 进程超过 50ms 的任务 PerformanceLongTaskTiming
    • 'element' - 特定元素的性能 PerformanceElementTiming

buffered: true

  • 含义:是否处理缓冲区中的历史性能条目
  • 作用
    • 当设置为 true 时,会立即返回在调用 observe() 之前已经存在的性能条目
    • 当设置为 false 时,只观察调用 observe() 之后新产生的性能条目
  • 使用场景
    • 如果你希望在代码初始化时就能获取到页面加载早期的性能数据,应该设为 true
    • 如果只关心后续发生的性能事件,可以设为 false

initiatorType: fetch | xmlhttprequest

  • 含义PerformanceResourceTiming 对象中的一个属性,用于表示 资源是由谁/什么发起的请求(即资源的初始化器类型)。它可以帮助开发者分析页面资源加载的来源和依赖关系。
  • 作用
    • 筛选出通过 fetch 或 xmlhttprequest 发起的请求
  • 使用场景
    1. 性能优化分析
      • 可以统计哪些类型的资源加载最慢(例如 scriptcssimg)。
      • 检测是否有意外的 fetch/xmlhttprequest 请求影响性能。
    2. 错误监控
      • 结合 responseStatus(如我们代码所示),可以监控 AJAX/Fetch 请求是否失败(>= 400)。
initiatorType 的可能值及含义

以下是常见的 initiatorType 值及其对应的含义:

说明
"img"资源由 <img> 标签加载(例如 <img src="image.jpg">
"script"资源由 <script> 标签加载(例如 <script src="app.js">
"link"资源由 <link> 标签加载(例如 <link rel="stylesheet" href="style.css">
"css"资源由 CSS 规则加载(例如 @import "theme.css" 或 background: url(...)
"xmlhttprequest"资源由 XMLHttpRequest 请求加载(AJAX)
"fetch"资源由 fetch() 请求加载
"iframe"资源由 <iframe>src 加载(例如 <iframe src="page.html">
"navigation"资源是页面本身(即 HTML 文档的加载)
"audio" / "video"资源由 <audio> 或 <video> 标签加载
"beacon"资源由 navigator.sendBeacon() 发送
"other"其他未明确分类的请求

组合效果

{ type: 'resource', buffered: true } 表示:

  1. 观察所有资源加载的性能数据
  2. 立即获取已经存在的资源加载性能数据(而不仅仅是后续新发生的)

实际应用示例

buffered 应用:

// 这样设置可以获取到页面加载初期所有资源的性能数据
observer.observe({ 
  type: 'resource', 
  buffered: true 
});

// 如果只想观察后续发生的资源加载
observer.observe({ 
  type: 'resource', 
  buffered: false 
});

前端监控:

// 1. 监控关键资源
const observer = new PerformanceObserver((list) => {
    list.getEntries()
        .filter(entry => entry.initiatorType === 'script')
        .forEach(script => {
            if (script.duration > 1000) {
                console.warn(`脚本加载过慢: ${script.name}`, script);
            }
        });
});

// 2. 分析图片性能
const imageTimings = performance.getEntriesByType('resource')
    .filter(entry => entry.initiatorType === 'img')
    .map(img => ({
        url: img.name,
        size: img.decodedBodySize,
        loadTime: img.duration,
        isLCPCandidate: img.renderBlockingStatus === 'blocking'
    }));

// 3. API性能监控
const apiCalls = performance.getEntriesByType('resource')
    .filter(entry => 
        entry.initiatorType === 'xmlhttprequest' || 
        entry.initiatorType === 'fetch'
    )
    .map(api => ({
        endpoint: new URL(api.name).pathname,
        duration: api.duration,
        responseSize: api.encodedBodySize
    }));

注意事项

  1. buffered: true 可能会一次性返回大量历史数据,要注意处理性能
  2. 不是所有性能条目类型都支持 buffered 选项
  3. 使用完毕后应调用 observer.disconnect() 停止观察

—— 完 🎉 敬请关注 “JavaScript与编程艺术”——