Skip to content

Commit 5c07774

Browse files
cmd-obcursoragent
andauthored
fix: Mockserver abort error (MetaMask#27262)
<!-- CURSOR_AGENT_PR_BODY_BEGIN --> Fixes `IncomingMessage.failWithAbortError` in the mockserver by safely handling aborted connections when reading request bodies. --- [Slack Thread](https://consensys.slack.com/archives/C02U025CVU4/p1773130024614199?thread_ts=1773130024.614199&cid=C02U025CVU4) <p><a href="https://cursor.com/agents/bc-f5115996-b2ea-5922-8a19-f5fa03ed7ff8"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-web-light.png"><img alt="Open in Web" width="114" height="28" src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a href="https://cursor.com/background-agent?bcId=bc-f5115996-b2ea-5922-8a19-f5fa03ed7ff8"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img alt="Open in Cursor" width="131" height="28" src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a>&nbsp;</p> <!-- CURSOR_AGENT_PR_BODY_END --> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: cmd-ob <cmd-ob@users.noreply.github.com>
1 parent 6f10e3c commit 5c07774

4 files changed

Lines changed: 95 additions & 35 deletions

File tree

tests/api-mocking/MockServerE2E.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,31 @@ const logger = createLogger({
2727
name: 'MockServer',
2828
level: LogLevel.INFO,
2929
});
30+
31+
/**
32+
* Safely reads request body text, catching abort errors.
33+
* When a client drops a connection mid-request (e.g., app navigation, AbortController),
34+
* mockttp's streamToBuffer rejects with Error('Aborted'). This wrapper catches those
35+
* errors and returns undefined instead of letting them bubble up as unhandled rejections.
36+
*
37+
* @param request - The mockttp request object
38+
* @returns The body text or undefined if reading failed or was aborted
39+
*/
40+
export const safeGetBodyText = async (request: {
41+
body: { getText: () => Promise<string | undefined> };
42+
}): Promise<string | undefined> => {
43+
try {
44+
return await request.body.getText();
45+
} catch (error) {
46+
if (error instanceof Error && error.message === 'Aborted') {
47+
logger.debug('Request body read aborted (client disconnected)');
48+
return undefined;
49+
}
50+
logger.warn('Failed to read request body:', error);
51+
return undefined;
52+
}
53+
};
54+
3055
interface LiveRequest {
3156
url: string;
3257
method: string;
@@ -461,11 +486,17 @@ export default class MockServerE2E implements Resource {
461486
}
462487

463488
try {
489+
// Read body safely before passing to handleDirectFetch to catch abort errors
490+
const bodyText = await safeGetBodyText(request);
491+
// If body read was aborted, return 499 (client closed request)
492+
if (request.method === 'POST' && bodyText === undefined) {
493+
return { statusCode: 499, body: '' };
494+
}
464495
return await handleDirectFetch(
465496
translatedUrl,
466497
request.method,
467498
request.headers,
468-
await request.body.getText(),
499+
bodyText,
469500
);
470501
} catch (error) {
471502
// Client dropped the connection before we could respond (e.g. bridge

tests/api-mocking/helpers/mockHelpers.ts

Lines changed: 47 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
MockEventsObject,
88
} from '../../framework';
99
import { getDecodedProxiedURL } from '../../smoke/notifications/utils/helpers.ts';
10+
import { safeGetBodyText } from '../MockServerE2E.ts';
1011

1112
// Creates a logger with INFO level as the mockServer produces too much noise
1213
// Change this to DEBUG as needed
@@ -252,30 +253,27 @@ export const setupMockPostRequest = async (
252253
}
253254

254255
// If URL matches, also check if request body matches (ignoring specified fields)
255-
try {
256-
const requestBodyText = await request.body.getText();
257-
const result = processPostRequestBody(requestBodyText, requestBody, {
258-
ignoreFields,
259-
});
260-
261-
if (!result.matches) {
262-
logger.warn('❌ Request body validation failed for', decodedUrl);
263-
logger.debug('Expected:', requestBody);
264-
logger.debug('Received:', result.requestBodyJson);
265-
logger.debug('Ignored fields:', ignoreFields);
266-
logger.debug('Error:', result.error);
267-
}
256+
// Use safeGetBodyText to handle abort errors gracefully
257+
const requestBodyText = await safeGetBodyText(request);
268258

269-
return result.matches;
270-
} catch (error) {
271-
// If we can't read the body, log the error and don't match
272-
// This prevents incorrect mock selection when body processing fails
273-
logger.error(
274-
'Failed to read request body during mock matching:',
275-
error,
276-
);
259+
// If body read failed/aborted, don't match this mock
260+
if (requestBodyText === undefined) {
277261
return false;
278262
}
263+
264+
const result = processPostRequestBody(requestBodyText, requestBody, {
265+
ignoreFields,
266+
});
267+
268+
if (!result.matches) {
269+
logger.warn('❌ Request body validation failed for', decodedUrl);
270+
logger.debug('Expected:', requestBody);
271+
logger.debug('Received:', result.requestBodyJson);
272+
logger.debug('Ignored fields:', ignoreFields);
273+
logger.debug('Error:', result.error);
274+
}
275+
276+
return result.matches;
279277
})
280278
.asPriority(priority) // Adding priority to this mock request helper as we want TestSpecificMocks to always take precedence
281279
.thenCallback(async (request) => {
@@ -324,17 +322,35 @@ export const interceptProxyUrl = async (
324322
}
325323
}
326324

327-
const response = await fetch(transformedUrl, {
328-
method: request.method,
329-
headers,
330-
body:
331-
request.method === 'POST' ? await request.body.getText() : undefined,
332-
});
325+
// Use safeGetBodyText to handle abort errors gracefully
326+
let bodyText: string | undefined;
327+
if (request.method === 'POST') {
328+
bodyText = await safeGetBodyText(request);
329+
// If body read was aborted, return 499 (client closed request)
330+
if (bodyText === undefined) {
331+
return { statusCode: 499, body: '' };
332+
}
333+
}
333334

334-
return {
335-
statusCode: response.status,
336-
body: await response.text(),
337-
};
335+
try {
336+
const response = await fetch(transformedUrl, {
337+
method: request.method,
338+
headers,
339+
body: bodyText,
340+
});
341+
342+
return {
343+
statusCode: response.status,
344+
body: await response.text(),
345+
};
346+
} catch (error) {
347+
// Handle fetch errors (e.g., network issues)
348+
logger.error('Error forwarding request:', error);
349+
return {
350+
statusCode: 502,
351+
body: JSON.stringify({ error: 'Failed to forward request' }),
352+
};
353+
}
338354
});
339355
};
340356

tests/api-mocking/mock-responses/polymarket/polymarket-mocks.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import { Mockttp } from 'mockttp';
66
import { setupMockRequest } from '../../helpers/mockHelpers.ts';
7+
import { safeGetBodyText } from '../../MockServerE2E.ts';
78
import {
89
POLYMARKET_CURRENT_POSITIONS_RESPONSE,
910
POLYMARKET_RESOLVED_LOST_POSITIONS_RESPONSE,
@@ -437,7 +438,10 @@ export const POLYMARKET_PRICES_MOCKS = async (mockServer: Mockttp) => {
437438
})
438439
.asPriority(PRIORITY.BASE)
439440
.thenCallback(async (request) => {
440-
const bodyText = await request.body.getText();
441+
const bodyText = await safeGetBodyText(request);
442+
if (bodyText === undefined) {
443+
return { statusCode: 499, body: '' };
444+
}
441445
const body = bodyText ? JSON.parse(bodyText) : [];
442446

443447
// Extract unique token IDs from the request
@@ -787,7 +791,10 @@ export const POLYMARKET_USDC_BALANCE_MOCKS = async (
787791
})
788792
.asPriority(PRIORITY.BASE)
789793
.thenCallback(async (request) => {
790-
const bodyText = await request.body.getText();
794+
const bodyText = await safeGetBodyText(request);
795+
if (bodyText === undefined) {
796+
return { statusCode: 499, body: '' };
797+
}
791798
const body = bodyText ? JSON.parse(bodyText) : undefined;
792799

793800
// Return appropriate mock response based on the call
@@ -1248,7 +1255,10 @@ export const POLYMARKET_UPDATE_USDC_BALANCE_MOCKS = async (
12481255
})
12491256
.asPriority(PRIORITY.BALANCE_REFRESH_PROXY) // Higher priority (1005) to catch balance refresh calls before base mocks
12501257
.thenCallback(async (request) => {
1251-
const bodyText = await request.body.getText();
1258+
const bodyText = await safeGetBodyText(request);
1259+
if (bodyText === undefined) {
1260+
return { statusCode: 499, body: '' };
1261+
}
12521262
const body = bodyText ? JSON.parse(bodyText) : undefined;
12531263

12541264
// Return the current global balance (not a captured value)

tests/framework/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ export { Logger, createLogger, LogLevel, logger } from './logger.ts';
1313
export { default as PortManager, ResourceType } from './PortManager.ts';
1414
export * from './types.ts';
1515

16+
// Mock server utilities
17+
export { safeGetBodyText } from '../api-mocking/MockServerE2E.ts';
18+
1619
// Dapp server exports for standalone usage (e.g., Appwright tests)
1720
export { default as DappServer } from './DappServer.ts';
1821
export { DappVariants, TestDapps } from './Constants.ts';

0 commit comments

Comments
 (0)