@@ -326,26 +326,55 @@ func uploadToS3(ctx context.Context, log *progress.Logger, payload *Payload) err
326326 return fmt .Errorf ("empty upload URL in response" )
327327 }
328328
329- // Upload payload to S3
330- log .Progress ("Uploading telemetry to S3..." )
331- putReq , err := http .NewRequestWithContext (ctx , http .MethodPut , urlResp .UploadURL , bytes .NewReader (payloadJSON ))
332- if err != nil {
333- return fmt .Errorf ("creating S3 PUT request: %w" , err )
334- }
335- putReq .Header .Set ("Content-Type" , "application/json" )
329+ // Upload payload to S3 with retry — use a longer timeout since payloads
330+ // with npm scan data and execution logs can be several MB.
331+ log .Progress ("Uploading telemetry to S3 (%d bytes)..." , len (payloadJSON ))
332+ s3Client := & http.Client {Timeout : 60 * time .Second }
333+ const maxRetries = 3
334+ var putResp * http.Response
335+ for attempt := 1 ; attempt <= maxRetries ; attempt ++ {
336+ uploadStart := time .Now ()
337+ putReq , reqErr := http .NewRequestWithContext (ctx , http .MethodPut , urlResp .UploadURL , bytes .NewReader (payloadJSON ))
338+ if reqErr != nil {
339+ return fmt .Errorf ("creating S3 PUT request: %w" , reqErr )
340+ }
341+ putReq .Header .Set ("Content-Type" , "application/json" )
336342
337- putResp , err := client .Do (putReq )
338- if err != nil {
339- return fmt .Errorf ("uploading to S3: %w" , err )
343+ putResp , err = s3Client .Do (putReq )
344+ elapsed := time .Since (uploadStart )
345+
346+ if err == nil && putResp .StatusCode == http .StatusOK {
347+ log .Progress ("Uploaded to S3 in %s" , elapsed )
348+ break
349+ }
350+
351+ // Clean up response body before retry
352+ if putResp != nil {
353+ _ , _ = io .Copy (io .Discard , putResp .Body )
354+ _ = putResp .Body .Close ()
355+ }
356+
357+ if attempt == maxRetries {
358+ if err != nil {
359+ return fmt .Errorf ("uploading to S3 (payload: %d bytes, elapsed: %s, attempts: %d): %w" ,
360+ len (payloadJSON ), elapsed , maxRetries , err )
361+ }
362+ return fmt .Errorf ("S3 upload failed with status %d (payload: %d bytes, attempts: %d)" ,
363+ putResp .StatusCode , len (payloadJSON ), maxRetries )
364+ }
365+
366+ // Log retry and backoff
367+ backoff := time .Duration (attempt ) * 2 * time .Second
368+ if err != nil {
369+ log .Progress ("S3 upload attempt %d/%d failed (%s), retrying in %s..." , attempt , maxRetries , elapsed , backoff )
370+ } else {
371+ log .Progress ("S3 upload attempt %d/%d got status %d, retrying in %s..." , attempt , maxRetries , putResp .StatusCode , backoff )
372+ }
373+ time .Sleep (backoff )
340374 }
341375 defer func () { _ = putResp .Body .Close () }()
342376 _ , _ = io .Copy (io .Discard , putResp .Body )
343377
344- if putResp .StatusCode != http .StatusOK {
345- return fmt .Errorf ("S3 upload failed with status %d" , putResp .StatusCode )
346- }
347- log .Progress ("Uploaded to S3" )
348-
349378 // Notify backend
350379 log .Progress ("Notifying backend of upload..." )
351380 notifyBody , _ := json .Marshal (map [string ]string {
0 commit comments