Skip to content

Commit fbb761a

Browse files
authored
@W-22517579 socket hangup fix (#15)
1 parent 383981e commit fbb761a

1 file changed

Lines changed: 44 additions & 16 deletions

File tree

src/commands/data/setup/transfer.ts

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,15 @@ export default class SetupTransfer extends SfCommand<SetupTransferResult> {
137137
}),
138138
};
139139

140+
private static readonly HTTP_TIMEOUT_MS = 20 * 60 * 1000;
141+
142+
private static isSocketHangUp(error: unknown): boolean {
143+
const code = (error as { code?: unknown } | null)?.code;
144+
if (code === 'ECONNRESET' || code === 'UND_ERR_SOCKET') return true;
145+
const message = error instanceof Error ? error.message : String(error);
146+
return /socket hang up|ECONNRESET/i.test(message);
147+
}
148+
140149
private static validateFlags(
141150
definitionIdentifier: string | undefined,
142151
version: string | undefined,
@@ -263,14 +272,22 @@ export default class SetupTransfer extends SfCommand<SetupTransferResult> {
263272
const instanceUrl = sourceConnection.instanceUrl ?? '';
264273
const fullUrl = `${instanceUrl}${exportApiPath}`;
265274

266-
const httpResponse = await fetch(fullUrl, {
267-
method: 'POST',
268-
headers: {
269-
'Content-Type': 'application/json',
270-
Authorization: `Bearer ${sourceConnection.accessToken ?? ''}`,
271-
},
272-
body: JSON.stringify(exportPayload),
273-
});
275+
const exportAbort = new AbortController();
276+
const exportTimeoutId = setTimeout(() => exportAbort.abort(), SetupTransfer.HTTP_TIMEOUT_MS);
277+
let httpResponse: Response;
278+
try {
279+
httpResponse = await fetch(fullUrl, {
280+
method: 'POST',
281+
headers: {
282+
'Content-Type': 'application/json',
283+
Authorization: `Bearer ${sourceConnection.accessToken ?? ''}`,
284+
},
285+
body: JSON.stringify(exportPayload),
286+
signal: exportAbort.signal,
287+
});
288+
} finally {
289+
clearTimeout(exportTimeoutId);
290+
}
274291

275292
const rawBody = await httpResponse.text();
276293

@@ -317,14 +334,25 @@ export default class SetupTransfer extends SfCommand<SetupTransferResult> {
317334
this.spinner.status = messages.getMessage('info.callingImportApi');
318335
const targetApiVersion = targetConnection.version;
319336
const importApiPath = `/services/data/v${targetApiVersion}/connect/industries/setup/dataset/actions/import`;
320-
const importResponse = await targetConnection.request<unknown>({
321-
method: 'POST',
322-
url: importApiPath,
323-
body: JSON.stringify(importPayload),
324-
headers: {
325-
'Content-Type': 'application/json',
326-
},
327-
});
337+
let importResponse: unknown = null;
338+
try {
339+
importResponse = await targetConnection.request<unknown>(
340+
{
341+
method: 'POST',
342+
url: importApiPath,
343+
body: JSON.stringify(importPayload),
344+
headers: {
345+
'Content-Type': 'application/json',
346+
},
347+
},
348+
{ timeout: SetupTransfer.HTTP_TIMEOUT_MS }
349+
);
350+
} catch (importError) {
351+
// Proxies/load balancers in front of scratch-org dataplane endpoints often close
352+
// the connection at ~100–120s while the server is still processing, so the client
353+
// sees ECONNRESET / "socket hang up" even when the import itself succeeded.
354+
if (!SetupTransfer.isSocketHangUp(importError)) throw importError;
355+
}
328356

329357
this.spinner.stop();
330358
this.log(messages.getMessage('info.success'));

0 commit comments

Comments
 (0)