You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Chrome扩展Manifest V3下HTTP请求被丢弃问题排查

问题概述
  • 开发了一款用于数据爬取的Chrome扩展,需调用外部API拉取数据,接口响应耗时不等,快时可即时返回,慢时约需4秒,使用场景下通常会连续快速发起5-10次请求。
  • 此前Manifest V3版本的service worker会随机终止运行,导致大量请求被丢弃,已针对该问题完成修复;后续又发现local storage未提供规范队列机制会引发竞态条件,也完成了对应适配。
  • 现存问题:完成上述两项修复后,请求仍存在被丢弃的情况:外部API已成功返回正确响应数据,但扩展侧始终无法接收到返回结果,需要可行的排查方向指引。
  • 以下附上相关实现代码,可供遇到同类service worker保活、本地存储队列问题的开发者参考。

本地存储队列实现

let writing: Map<string, Promise<any>> = new Map();

let updateUnsynchronized = async (ks: string[], f: Function) => {
  let m = await new Promise((resolve, reject) => {
    chrome.storage.local.get(ks, res => {
      let m = {};
      for (let k of ks) {
        m[k] = res[k];
      }
      maybeResolveLocalStorage(resolve, reject, m);
    });
  });
  // Guaranteed to have not changed in the meantime
  let updated = await new Promise((resolve, reject) => {
    let updateMap = f(m);
    chrome.storage.local.set(updateMap, () => {
      maybeResolveLocalStorage(resolve, reject, updateMap);
    });
  });
  console.log(ks, 'Updated', updated);
  return updated;
};

export async function update(ks: string[], f: Function) {
  let ret = null;

  // Global lock for now
  await navigator.locks.request('global-storage-lock', async lock => {
    ret = await updateUnsynchronized(ks, f);
  });

  return ret;
}

核心业务函数实现

export async function appendStoredScrapes(
  scrape: any,
  fromHTTPResponse: boolean
) {
  let updated = await update(['urlType', 'scrapes'], storage => {
    const urlType = storage.urlType;
    const scrapes = storage.scrapes;
    const {url} = scrape;

    if (fromHTTPResponse) {
      // We want to make sure that the url type at time of scrape, not time of return, is used
      scrapes[url] = {...scrapes[url], ...scrape};
    } else {
      scrapes[url] = {...scrapes[url], ...scrape, urlType};
    }

    return {scrapes};
  });

  chrome.action.setBadgeText({text: `${Object.keys(updated['scrapes']).length}`});
}

Service Worker保活实现

let defaultKeepAliveInterval = 20000;

// To avoid GC
let channel;

// To be run in content scripts
export function contentKeepAlive(name : string) {
  channel = chrome.runtime.connect({ name });

  channel.onDisconnect.addListener(() => contentKeepAlive(name));

  channel.onMessage.addListener(msg => { });
}

let deleteTimer = (chan : any) => {
  if (chan._timer) {
    clearTimeout(chan._timer);
    delete chan._timer;
  }
}

let backgroundForceReconnect = (chan : chrome.runtime.Port) => {
  deleteTimer(chan);
  chan.disconnect();
}

// To be run in background scripts
export function backgroundKeepAlive(name : string) {
  chrome.runtime.onConnect.addListener(chan => {
    if (chan.name === name) {
      channel = chan;
      channel.onMessage.addListener((msg, chan) => { });
      channel.onDisconnect.addListener(deleteTimer);
      channel._timer = setTimeout(backgroundForceReconnect, defaultKeepAliveInterval, channel);
    }
  });
}

// 注意:MV3中chrome.runtime.onMessage监听器即使不需要返回响应,也必须调用sendResponse
export function defaultSendResponse (sendResponse : Function) {
  sendResponse({ farewell: 'goodbye' });
}

background.ts相关逻辑

backgroundKeepAlive('extension-background');

let listen = async (request, sender, sendResponse) => {
  try {
    if (request.message === 'SEND_URL_DETAIL') {
      const {url, website, urlType} = request;
      await appendStoredScrapes({url}, false);
      let data = await fetchPageData(url, website, urlType);
      console.log(data, url, 'fetch data returned background');
      await appendStoredScrapes(data, true);
      defaultSendResponse(sendResponse);
    } else if (request.message === 'KEEPALIVE') {
      sendResponse({isAlive: true});
    } else {
      defaultSendResponse(sendResponse);
    }
  } catch (e) {
    console.error('background listener error', e);
  }
};

chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) {
  listen(request, sender, sendResponse);
});

排查方向与修复方案

从现有代码看,请求丢失是几个明确的MV3机制适配问题导致的,按优先级排查修复即可:

  • 最高优先级:修复onMessage监听器返回值错误
    现有代码中listen是异步函数,但chrome.runtime.onMessage的监听器同步执行完后没有返回true,Chrome会判定该消息不需要异步响应,直接关闭消息端口。等后续fetch拿到结果、调用sendResponse时,端口已经失效,响应自然无法送达,这是API返回成功但扩展收不到结果的最核心原因。
    修复方式很简单,在监听器中添加返回值即可:
    chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) {
      listen(request, sender, sendResponse);
      return true; // 告知Chrome需要异步发送响应,保持消息端口开放
    });
    
  • 补全请求上下文持久化兜底
    现有逻辑完全依赖内存中的Promise链维护请求状态,一旦service worker在等待API响应的间隙被回收,即使API成功返回,重启后的service worker也没有之前的请求上下文,自然不会处理返回结果。需要在每次发起API请求前,先把请求元信息(请求地址、参数、发起时间、关联url)写入本地存储,service worker每次启动时先扫描存储中未完成的请求,做状态恢复或者重试,避免上下文丢失。
  • 修复保活逻辑的场景覆盖漏洞
    现有保活逻辑完全依赖content script发起长连接,如果请求是从popup面板、扩展右键菜单、devtools面板等场景触发,页面没有注入content script,长连接根本不会建立,service worker闲置30秒后依然会被系统回收。需要在批量请求发起阶段,主动在service worker侧启动间隔20秒的保活定时器,所有请求处理完成后再清除定时器,覆盖所有请求触发场景。
  • 优化本地存储锁机制,避免队列阻塞
    现有逻辑使用全局存储锁,连续发起5-10次请求时,所有存储操作都会排队等待,一旦某个存储操作因为service worker休眠被中断,整个锁会被一直占用,后续所有存储写入、响应处理逻辑全部卡住,表现为请求丢失。可以把全局锁替换为按存储key维度的细粒度锁,同时给锁增加5秒超时自动释放的兜底逻辑,避免单操作阻塞整个队列。
  • 给fetch增加超时和异常捕获
    现有逻辑没有给fetchPageData设置超时,也没有记录请求的中间状态,一旦fetch因为service worker销毁被中断,既不会触发异常,也没有重试机制。可以给fetch设置10秒超时,超时后根据存储中记录的未完成请求做重试,同时把API返回结果先写入存储,再通知前端页面,避免内存状态丢失。

内容的提问来源于stack exchange,提问作者dizzy

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 06:18:17