@@ -419,26 +419,55 @@ func uploadToS3(ctx context.Context, log *progress.Logger, payload *Payload) err
419419 return fmt .Errorf ("empty upload URL in response" )
420420 }
421421
422- // Upload payload to S3
423- log .Progress ("Uploading telemetry to S3..." )
424- putReq , err := http .NewRequestWithContext (ctx , http .MethodPut , urlResp .UploadURL , bytes .NewReader (payloadJSON ))
425- if err != nil {
426- return fmt .Errorf ("creating S3 PUT request: %w" , err )
427- }
428- putReq .Header .Set ("Content-Type" , "application/json" )
422+ // Upload payload to S3 with retry — use a longer timeout since payloads
423+ // with npm scan data and execution logs can be several MB.
424+ log .Progress ("Uploading telemetry to S3 (%d bytes)..." , len (payloadJSON ))
425+ s3Client := & http.Client {Timeout : 60 * time .Second }
426+ const maxRetries = 3
427+ var putResp * http.Response
428+ for attempt := 1 ; attempt <= maxRetries ; attempt ++ {
429+ uploadStart := time .Now ()
430+ putReq , reqErr := http .NewRequestWithContext (ctx , http .MethodPut , urlResp .UploadURL , bytes .NewReader (payloadJSON ))
431+ if reqErr != nil {
432+ return fmt .Errorf ("creating S3 PUT request: %w" , reqErr )
433+ }
434+ putReq .Header .Set ("Content-Type" , "application/json" )
429435
430- putResp , err := client .Do (putReq )
431- if err != nil {
432- return fmt .Errorf ("uploading to S3: %w" , err )
436+ putResp , err = s3Client .Do (putReq )
437+ elapsed := time .Since (uploadStart )
438+
439+ if err == nil && putResp .StatusCode == http .StatusOK {
440+ log .Progress ("Uploaded to S3 in %s" , elapsed )
441+ break
442+ }
443+
444+ // Clean up response body before retry
445+ if putResp != nil {
446+ _ , _ = io .Copy (io .Discard , putResp .Body )
447+ _ = putResp .Body .Close ()
448+ }
449+
450+ if attempt == maxRetries {
451+ if err != nil {
452+ return fmt .Errorf ("uploading to S3 (payload: %d bytes, elapsed: %s, attempts: %d): %w" ,
453+ len (payloadJSON ), elapsed , maxRetries , err )
454+ }
455+ return fmt .Errorf ("S3 upload failed with status %d (payload: %d bytes, attempts: %d)" ,
456+ putResp .StatusCode , len (payloadJSON ), maxRetries )
457+ }
458+
459+ // Log retry and backoff
460+ backoff := time .Duration (attempt ) * 2 * time .Second
461+ if err != nil {
462+ log .Progress ("S3 upload attempt %d/%d failed (%s), retrying in %s..." , attempt , maxRetries , elapsed , backoff )
463+ } else {
464+ log .Progress ("S3 upload attempt %d/%d got status %d, retrying in %s..." , attempt , maxRetries , putResp .StatusCode , backoff )
465+ }
466+ time .Sleep (backoff )
433467 }
434468 defer func () { _ = putResp .Body .Close () }()
435469 _ , _ = io .Copy (io .Discard , putResp .Body )
436470
437- if putResp .StatusCode != http .StatusOK {
438- return fmt .Errorf ("S3 upload failed with status %d" , putResp .StatusCode )
439- }
440- log .Progress ("Uploaded to S3" )
441-
442471 // Notify backend
443472 log .Progress ("Notifying backend of upload..." )
444473 notifyBody , _ := json .Marshal (map [string ]string {
0 commit comments