Skip to content

Commit 96a29cb

Browse files
fix: Make ResponseWrapper an instance of Response (#2889)
Overwrite `[Symbol.hasInstance]` on the exposed `Response` class slightly to make `ResponseWrapper` pass `instanceof` checks. This prevents `fetch` responses from failing when using WASM bindgen for instance. Fixes #2891
1 parent 2d6575e commit 96a29cb

File tree

5 files changed

+84
-10
lines changed

5 files changed

+84
-10
lines changed
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
2-
"branches": 80,
3-
"functions": 90.06,
4-
"lines": 90.75,
5-
"statements": 90.12
2+
"branches": 80.27,
3+
"functions": 90.13,
4+
"lines": 90.77,
5+
"statements": 90.15
66
}

packages/snaps-execution-environments/src/common/BaseSnapExecutor.test.browser.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -999,6 +999,57 @@ describe('BaseSnapExecutor', () => {
999999
});
10001000
});
10011001

1002+
it('uses a response for fetch that works with instanceof', async () => {
1003+
const CODE = `
1004+
module.exports.onRpcRequest = () => fetch('https://metamask.io').then(res => res instanceof Response);
1005+
`;
1006+
1007+
const fetchSpy = spy(globalThis, 'fetch');
1008+
1009+
fetchSpy.mockImplementation(async () => {
1010+
return new Response('foo');
1011+
});
1012+
1013+
const executor = new TestSnapExecutor();
1014+
await executor.executeSnap(1, MOCK_SNAP_ID, CODE, ['fetch', 'Response']);
1015+
1016+
expect(await executor.readCommand()).toStrictEqual({
1017+
jsonrpc: '2.0',
1018+
id: 1,
1019+
result: 'OK',
1020+
});
1021+
1022+
await executor.writeCommand({
1023+
jsonrpc: '2.0',
1024+
id: 2,
1025+
method: 'snapRpc',
1026+
params: [
1027+
MOCK_SNAP_ID,
1028+
HandlerType.OnRpcRequest,
1029+
MOCK_ORIGIN,
1030+
{ jsonrpc: '2.0', method: '', params: [] },
1031+
],
1032+
});
1033+
1034+
expect(await executor.readCommand()).toStrictEqual({
1035+
jsonrpc: '2.0',
1036+
method: 'OutboundRequest',
1037+
params: { source: 'fetch' },
1038+
});
1039+
1040+
expect(await executor.readCommand()).toStrictEqual({
1041+
jsonrpc: '2.0',
1042+
method: 'OutboundResponse',
1043+
params: { source: 'fetch' },
1044+
});
1045+
1046+
expect(await executor.readCommand()).toStrictEqual({
1047+
id: 2,
1048+
jsonrpc: '2.0',
1049+
result: true,
1050+
});
1051+
});
1052+
10021053
it('notifies execution service of out of band errors via unhandledrejection', async () => {
10031054
const CODE = `
10041055
module.exports.onRpcRequest = async () => 'foo';

packages/snaps-execution-environments/src/common/endowments/endowments.test.browser.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import CryptoEndowment from './crypto';
1212
import date from './date';
1313
import interval from './interval';
1414
import math from './math';
15-
import network from './network';
15+
import network, { ResponseWrapper } from './network';
1616
import timeout from './timeout';
1717

1818
// @ts-expect-error - `globalThis.process` is not optional.
@@ -218,6 +218,19 @@ describe('endowments', () => {
218218
endowments: { fetchAttenuated },
219219
factory: () => undefined,
220220
},
221+
ResponseWrapper: {
222+
endowments: {
223+
Response: ResponseHardened,
224+
ResponseWrapper: harden(ResponseWrapper),
225+
},
226+
factory: () =>
227+
new ResponseWrapper(
228+
new Response(),
229+
{ lastTeardown: 0 },
230+
async () => undefined,
231+
async () => undefined,
232+
),
233+
},
221234
};
222235

223236
Object.entries(TEST_ENDOWMENTS).forEach(

packages/snaps-execution-environments/src/common/endowments/network.test.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import fetchMock from 'jest-fetch-mock';
22

3-
import network from './network';
3+
import network, { ResponseWrapper } from './network';
44

55
describe('Network endowments', () => {
66
beforeAll(() => {
@@ -180,14 +180,15 @@ describe('Network endowments', () => {
180180
const RESULT = 'OK';
181181
fetchMock.mockOnce(async () => Promise.resolve(RESULT));
182182

183-
const { fetch } = network.factory(factoryOptions);
183+
const { fetch, Response } = network.factory(factoryOptions);
184184
const result = await fetch('foo.com');
185185

186186
expect(result.bodyUsed).toBe(false);
187187
const clonedResult = result.clone();
188188
expect(clonedResult).toBeDefined();
189189
expect(await clonedResult.text()).toBe(RESULT);
190-
expect(clonedResult).not.toBeInstanceOf(Response);
190+
expect(clonedResult).toBeInstanceOf(Response);
191+
expect(clonedResult).toBeInstanceOf(ResponseWrapper);
191192
});
192193

193194
it('should return when json is called', async () => {

packages/snaps-execution-environments/src/common/endowments/network.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type { EndowmentFactoryOptions } from './commonEndowmentFactory';
77
* This class wraps a Response object.
88
* That way, a teardown process can stop any processes left.
99
*/
10-
class ResponseWrapper implements Response {
10+
export class ResponseWrapper implements Response {
1111
readonly #teardownRef: { lastTeardown: number };
1212

1313
#ogResponse: Response;
@@ -145,6 +145,15 @@ class ResponseWrapper implements Response {
145145
}
146146
}
147147

148+
// We redefine the global Response class to overwrite [Symbol.hasInstance].
149+
// This fixes problems where the response from `fetch` would not pass
150+
// instance of checks, leading to failures in WASM bindgen.
151+
class AlteredResponse extends Response {
152+
static [Symbol.hasInstance](instance: unknown) {
153+
return instance instanceof Response || instance instanceof ResponseWrapper;
154+
}
155+
}
156+
148157
/**
149158
* Create a network endowment, consisting of a `fetch` function.
150159
* This allows us to provide a teardown function, so that we can cancel
@@ -297,7 +306,7 @@ const createNetwork = ({ notify }: EndowmentFactoryOptions = {}) => {
297306
// These endowments are not (and should never be) available by default.
298307
Request: harden(Request),
299308
Headers: harden(Headers),
300-
Response: harden(Response),
309+
Response: harden(AlteredResponse),
301310
teardownFunction,
302311
};
303312
};

0 commit comments

Comments
 (0)