@@ -437,61 +437,9 @@ export class HippoDid {
437437 headers [ "X-Tenant-Id" ] = this . tenantId ;
438438 }
439439
440- let lastError : Error | undefined ;
441-
442- for ( let attempt = 0 ; attempt <= this . maxRetries ; attempt ++ ) {
443- if ( attempt > 0 ) {
444- const backoff = INITIAL_BACKOFF_MS * Math . pow ( 2 , attempt - 1 ) ;
445- const jitter = Math . random ( ) * backoff * 0.3 ;
446- await sleep ( backoff + jitter ) ;
447- }
448-
449- let response : Response ;
450- try {
451- response = await fetch ( url , { method : "POST" , headers, body : form } ) ;
452- } catch ( err ) {
453- lastError = err instanceof Error ? err : new Error ( String ( err ) ) ;
454- continue ;
455- }
456-
457- if ( response . ok ) {
458- const text = await response . text ( ) ;
459- return JSON . parse ( text ) as T ;
460- }
461-
462- let errorMessage : string ;
463- try {
464- const errBody = await response . json ( ) ;
465- errorMessage = errBody . message ?? errBody . error ?? JSON . stringify ( errBody ) ;
466- } catch {
467- errorMessage = `HTTP ${ response . status } ` ;
468- }
469-
470- if ( response . status === 429 || response . status >= 500 ) {
471- if ( response . status === 429 && attempt === this . maxRetries ) {
472- const retryAfter = response . headers . get ( "Retry-After" ) ;
473- throw new RateLimitError (
474- errorMessage ,
475- retryAfter ? parseInt ( retryAfter , 10 ) * 1000 : 0 ,
476- ) ;
477- }
478- lastError = new HippoDidError ( errorMessage , response . status ) ;
479- continue ;
480- }
481-
482- if ( response . status === 401 || response . status === 403 ) {
483- throw new AuthenticationError ( errorMessage ) ;
484- }
485- if ( response . status === 404 ) {
486- throw new NotFoundError ( errorMessage ) ;
487- }
488- if ( response . status === 400 || response . status === 422 ) {
489- throw new ValidationError ( errorMessage , response . status ) ;
490- }
491- throw new HippoDidError ( errorMessage , response . status ) ;
492- }
493-
494- throw lastError ?? new HippoDidError ( "Request failed after retries" ) ;
440+ return this . executeWithRetry (
441+ ( ) => fetch ( url , { method : "POST" , headers, body : form } ) ,
442+ ) ;
495443 }
496444
497445 private async request < T > (
@@ -503,15 +451,30 @@ export class HippoDid {
503451 const url = `${ this . baseUrl } ${ path } ` ;
504452 const headers : Record < string , string > = {
505453 Authorization : `Bearer ${ this . apiKey } ` ,
506- "Content-Type" : "application/json" ,
507454 Accept : "application/json" ,
508455 ...extraHeaders ,
509456 } ;
510-
457+ if ( body != null ) {
458+ headers [ "Content-Type" ] = "application/json" ;
459+ }
511460 if ( this . tenantId ) {
512461 headers [ "X-Tenant-Id" ] = this . tenantId ;
513462 }
514463
464+ return this . executeWithRetry (
465+ ( ) =>
466+ fetch ( url , {
467+ method,
468+ headers,
469+ body : body != null ? JSON . stringify ( body ) : undefined ,
470+ } ) ,
471+ ) ;
472+ }
473+
474+ /** Shared retry/backoff/error-dispatch loop used by request() and requestMultipart(). */
475+ private async executeWithRetry < T > (
476+ doFetch : ( ) => Promise < Response > ,
477+ ) : Promise < T > {
515478 let lastError : Error | undefined ;
516479
517480 for ( let attempt = 0 ; attempt <= this . maxRetries ; attempt ++ ) {
@@ -523,11 +486,7 @@ export class HippoDid {
523486
524487 let response : Response ;
525488 try {
526- response = await fetch ( url , {
527- method,
528- headers,
529- body : body != null ? JSON . stringify ( body ) : undefined ,
530- } ) ;
489+ response = await doFetch ( ) ;
531490 } catch ( err ) {
532491 lastError = err instanceof Error ? err : new Error ( String ( err ) ) ;
533492 continue ;
@@ -540,7 +499,6 @@ export class HippoDid {
540499 return JSON . parse ( text ) as T ;
541500 }
542501
543- // Parse error body
544502 let errorMessage : string ;
545503 try {
546504 const errBody = await response . json ( ) ;
@@ -550,7 +508,6 @@ export class HippoDid {
550508 errorMessage = `HTTP ${ response . status } ` ;
551509 }
552510
553- // Retryable: 429 or 5xx
554511 if ( response . status === 429 || response . status >= 500 ) {
555512 if ( response . status === 429 && attempt === this . maxRetries ) {
556513 const retryAfter = response . headers . get ( "Retry-After" ) ;
@@ -563,7 +520,6 @@ export class HippoDid {
563520 continue ;
564521 }
565522
566- // Non-retryable errors
567523 if ( response . status === 401 || response . status === 403 ) {
568524 throw new AuthenticationError ( errorMessage ) ;
569525 }
@@ -586,11 +542,11 @@ function normaliseBatchJob(raw: BatchJobApiResponse): BatchJob {
586542 jobId : raw . jobId ,
587543 status : raw . status ,
588544 dryRun : raw . dryRun ,
589- totalRows : raw . progress . totalRows ,
590- succeeded : raw . progress . succeeded ,
591- failed : raw . progress . failed ,
592- skipped : raw . progress . skipped ,
593- errors : raw . errors . map ( ( e ) => ( {
545+ totalRows : raw . progress ? .totalRows ?? 0 ,
546+ succeeded : raw . progress ? .succeeded ?? 0 ,
547+ failed : raw . progress ? .failed ?? 0 ,
548+ skipped : raw . progress ? .skipped ?? 0 ,
549+ errors : ( raw . errors ?? [ ] ) . map ( ( e ) => ( {
594550 rowIndex : e . rowIndex ,
595551 externalId : e . externalId ,
596552 message : e . message ,
@@ -604,7 +560,7 @@ function normaliseBatchJob(raw: BatchJobApiResponse): BatchJob {
604560/** Convert an array of row objects to a CSV string. */
605561function rowsToCsv ( rows : Record < string , string > [ ] ) : string {
606562 if ( rows . length === 0 ) return "" ;
607- const columns = Object . keys ( rows [ 0 ] ) ;
563+ const columns = [ ... new Set ( rows . flatMap ( Object . keys ) ) ] ;
608564 const escapeCsv = ( v : string ) => {
609565 if ( v . includes ( "," ) || v . includes ( '"' ) || v . includes ( "\n" ) ) {
610566 return `"${ v . replace ( / " / g, '""' ) } "` ;
0 commit comments