Skip to content

Commit 9fa7106

Browse files
tsemachhjunie-agent
andcommitted
fix(replayer): swallow detach errors during replay
Co-authored-by: Junie <junie@jetbrains.com>
1 parent 8f55dd0 commit 9fa7106

3 files changed

Lines changed: 32 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file.
55
The format is based on Keep a Changelog,
66
and this project adheres to Semantic Versioning.
77

8+
## [1.0.18] - 2026-05-03
9+
### Fixed
10+
- Replay no longer logs `Uncaught (in promise) Error: Detached while handling command` when the debugger detaches mid-flight (tab navigated/closed, replay stopped, or service worker recycled). All `chrome.debugger.sendCommand` calls in `src/background/replayer.ts` are now routed through a `safeSendCommand` helper that swallows detach-related errors. After a latency wait, replay state is re-checked before sending the response so we don't try to fulfill on a detached debugger.
11+
812
## [1.0.17] - 2026-05-03
913
### Changed
1014
- Captured Requests table now auto-sizes columns (▶ / Status / Method shrink to content; Path takes the remaining width with truncation + full-URL tooltip) so nothing overflows the popup.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "apireplay",
3-
"version": "1.0.17",
3+
"version": "1.0.18",
44
"private": true,
55
"type": "module",
66
"scripts": {

src/background/replayer.ts

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,34 @@ function methodsMatch(a: string | undefined, b: string | undefined): boolean {
4242
return (a || 'GET').toUpperCase() === (b || 'GET').toUpperCase();
4343
}
4444

45+
function isDetachedError(err: unknown): boolean {
46+
const msg = err instanceof Error ? err.message : String(err ?? '');
47+
return /Detached while handling command|Debugger is not attached|No tab with given id|Target closed/i.test(msg);
48+
}
49+
50+
async function safeSendCommand(tabId: number, method: string, commandParams?: object): Promise<void> {
51+
try {
52+
await chrome.debugger.sendCommand({ tabId }, method as never, commandParams as never);
53+
} catch (err) {
54+
if (isDetachedError(err)) {
55+
// Debugger was detached (tab closed, navigated, or replay stopped) while we were
56+
// handling the request. Swallow — there's nothing to fulfill anymore.
57+
return;
58+
}
59+
throw err;
60+
}
61+
}
62+
4563
async function continueRequest(tabId: number, message: string, params: ReplayerEventParams): Promise<void> {
4664
if (message === 'Fetch.requestPaused' && params.requestId) {
47-
await chrome.debugger.sendCommand({ tabId }, 'Fetch.continueRequest', {
65+
await safeSendCommand(tabId, 'Fetch.continueRequest', {
4866
requestId: params.requestId
4967
});
5068
return;
5169
}
5270

5371
if (params.interceptionId) {
54-
await chrome.debugger.sendCommand({ tabId }, 'Network.continueInterceptedRequest', {
72+
await safeSendCommand(tabId, 'Network.continueInterceptedRequest', {
5573
interceptionId: params.interceptionId
5674
});
5775
}
@@ -249,12 +267,17 @@ export async function onReplayerEvent(store: StateStore, tabId: number, message:
249267
const latency = resolveLatency(replayStatsData.replayOptions);
250268
if (latency > 0) {
251269
await new Promise((resolve) => setTimeout(resolve, latency));
270+
// Re-check after the wait — replay may have been stopped / debugger detached.
271+
const stateAfter = await store.get();
272+
if (!stateAfter.isReplaying) {
273+
return;
274+
}
252275
}
253276

254277
const rawBody = matched.responseBody || '';
255278

256279
if (message === 'Fetch.requestPaused' && params.requestId) {
257-
await chrome.debugger.sendCommand({ tabId }, 'Fetch.fulfillRequest', {
280+
await safeSendCommand(tabId, 'Fetch.fulfillRequest', {
258281
requestId: params.requestId,
259282
responseCode: matched.status || 200,
260283
responsePhrase: matched.statusText || 'OK',
@@ -266,7 +289,7 @@ export async function onReplayerEvent(store: StateStore, tabId: number, message:
266289

267290
const body = safeBase64(rawBody);
268291
if (params.interceptionId) {
269-
await chrome.debugger.sendCommand({ tabId }, 'Network.continueInterceptedRequest', {
292+
await safeSendCommand(tabId, 'Network.continueInterceptedRequest', {
270293
interceptionId: params.interceptionId,
271294
rawResponse: safeBase64(
272295
`HTTP/1.1 ${matched.status || 200} ${matched.statusText || 'OK'}\r\n` +

0 commit comments

Comments
 (0)