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
相关产品推荐
相关产品推荐

