Skip to content

Commit 322a595

Browse files
committed
fix: gracefully end sub-stream on port disconnect to prevent Premature close errors
1 parent d0295c3 commit 322a595

File tree

2 files changed

+65
-3
lines changed

2 files changed

+65
-3
lines changed

shared/modules/caip-stream.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,18 @@ class MockStream extends Duplex {
4040
}
4141
}
4242

43+
const createMockPort = () => {
44+
// Minimal chrome.runtime.Port‐like stub
45+
const listeners: (() => void)[] = [];
46+
return {
47+
onDisconnect: {
48+
addListener: (fn: () => void) => listeners.push(fn),
49+
// helper used by tests
50+
_trigger: () => listeners.forEach((fn) => fn()),
51+
},
52+
};
53+
};
54+
4355
describe('CAIP Stream', () => {
4456
describe('createCaipStream', () => {
4557
it('pipes and unwraps a caip-348 message from source stream to the substream', async () => {
@@ -92,5 +104,26 @@ describe('CAIP Stream', () => {
92104

93105
await expect(promise).resolves.toBe(undefined);
94106
});
107+
108+
it('does not emit an error on “Premature close” after graceful shutdown', async () => {
109+
const sourceStream = new MockStream();
110+
const mockPort = createMockPort();
111+
(sourceStream as unknown as { _port: typeof mockPort })._port = mockPort;
112+
113+
const providerStream = createCaipStream(sourceStream);
114+
115+
const errorSpy = jest.fn();
116+
providerStream.on('error', errorSpy);
117+
118+
// Trigger disconnect
119+
(
120+
mockPort as unknown as { onDisconnect: { _trigger: () => void } }
121+
).onDisconnect._trigger();
122+
123+
// Give the event loop a tick to propagate potential errors
124+
await new Promise((r) => setImmediate(r));
125+
126+
expect(errorSpy).not.toHaveBeenCalled();
127+
});
95128
});
96129
});

shared/modules/caip-stream.ts

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,38 @@ export class CaipStream extends Duplex {
6565
export const createCaipStream = (portStream: Duplex): Duplex => {
6666
const caipStream = new CaipStream();
6767

68-
pipeline(portStream, caipStream, portStream, (err: Error) => {
69-
caipStream.substream.destroy();
70-
console.log('MetaMask CAIP stream', err);
68+
/** Cleanly end the CAIP side if the port goes away. */
69+
const handlePortGone = () => {
70+
// End only once
71+
if (
72+
!caipStream.substream.destroyed &&
73+
!caipStream.substream.writableEnded
74+
) {
75+
caipStream.substream.end();
76+
}
77+
};
78+
79+
/* ---------- 1. Listen for tab / iframe shutdown signals ---------- */
80+
81+
// a. Node-style streams emit 'close' and/or 'end'
82+
portStream.once?.('close', handlePortGone);
83+
portStream.once?.('end', handlePortGone);
84+
85+
// b. chrome.runtime.Port exposes onDisconnect
86+
// (ExtensionPortStream exposes the raw Port at `._port`)
87+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
88+
const rawPort: chrome.runtime.Port | undefined = (portStream as any)?._port;
89+
rawPort?.onDisconnect?.addListener(handlePortGone);
90+
91+
/* ---------- 2. Wire up the full duplex pipeline ---------- */
92+
93+
pipeline(portStream, caipStream, portStream, (err: Error | null) => {
94+
caipStream.substream.destroy(); // full cleanup
95+
96+
// Ignore the normal premature-close that used to spam the console
97+
if (err && err.message !== 'Premature close') {
98+
console.error('MetaMask CAIP stream error:', err);
99+
}
71100
});
72101

73102
return caipStream.substream;

0 commit comments

Comments
 (0)