@@ -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 / s o c k e t h a n g u p | E C O N N R E S E T / 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