hashicorp/terraform · error

failed to upload part

Error message

failed to upload part: %s

What it means

After the workers complete, the code iterates the osUploadPartResponses channel and rejects any response whose error is non-nil OR whose partNumber/ETag is nil. Indicates at least one part-upload returned an error or a malformed success (missing ETag/partNumber). Note the format uses %s on response.error, which can be nil when only partNumber/ETag is missing — the message may read 'failed to upload part: %!s(<nil>)'.

Solutions

  1. Check worker logs (TF_LOG=DEBUG) for the per-part error — the aggregated message can hide it when response.error is nil.
  2. Reduce parallelism pressure — fewer/smaller parts (raise DefaultFilePartSize) lowers the chance of any single part failing.
  3. Refresh long-lived credentials before starting a large upload; multipart uploads can outlive a short token TTL.
  4. Retry the whole Put; the upload was aborted/abandoned on this path.

Example fix

// before: 10 workers upload parts against a saturated link -> intermittent 317
// after: raise part size to reduce part count and per-part bandwidth pressure
var DefaultFilePartSize int64 = 256 * 1024 * 1024
// and run with TF_LOG=DEBUG to capture the per-part error
//   terraform apply -auto-approve 2>tf.log
//   grep 'failed to upload part' tf.log
Defensive patterns

Strategy: retry

Type guard

func isPartUploadErr(err error) bool {
    return err != nil && strings.Contains(err.Error(), "failed to upload part")
}

Try / catch

// On per-part failure, capture the worker error (it may be nil when only ETag is missing)
if response.error != nil || response.partNumber == nil || response.response.ETag == nil {
    err := response.error
    if err == nil { err = fmt.Errorf("part %d upload returned nil ETag/partNumber", deref(response.partNumber)) }
    return nil, fmt.Errorf("failed to upload part: %w", err)
}

Prevention

When it happens

Trigger: A worker's UploadPart returned an OCI/transport error (throttling, auth expiry, 5xx); a worker returned success but with nil ETag or nil partNumber; transient failures under 10-way parallel uploads.

Common situations: Bandwidth saturated by 10 concurrent part uploads; token expiring during a long multipart upload; throttling (429) under high CI concurrency; partial network failure affecting one worker.

Related errors


AI-assisted analysis of hashicorp/terraform@d32a084675 (2026-08-11). Data as JSON: /api/errors/24599b29238a6e50. Report an issue: GitHub.

Appendix: source

Thrown at internal/backend/remote-state/oci/multipart_upload.go:126

			ctx.uploadPartsWorker()
		}()
	}

	wg.Wait()
	close(osUploadPartResponses)
	close(errChan)

	// Collect errors from workers
	for workerErr := range errChan {
		if workerErr != nil {
			return workerErr
		}
	}
	commitMultipartUploadPartDetails := make([]objectstorage.CommitMultipartUploadPartDetails, len(sourceBlocks))
	i := 0
	for response := range osUploadPartResponses {
		if response.error != nil || response.partNumber == nil || response.response.ETag == nil {
			return fmt.Errorf("failed to upload part: %s", response.error)
		}
		partNumber, etag := *response.partNumber, *response.response.ETag
		commitMultipartUploadPartDetails[i] = objectstorage.CommitMultipartUploadPartDetails{
			PartNum: common.Int(partNumber),
			Etag:    common.String(etag),
		}
		i++
	}

	if len(commitMultipartUploadPartDetails) != len(sourceBlocks) {
		abortReq := objectstorage.AbortMultipartUploadRequest{
			UploadId:      multipartUploadResponse.MultipartUpload.UploadId,
			NamespaceName: multipartUploadResponse.Namespace,
			BucketName:    multipartUploadResponse.Bucket,
			ObjectName:    multipartUploadResponse.Object,
		}
		_, abortErr := multipartUploadData.client.objectStorageClient.AbortMultipartUpload(context.Background(), abortReq)
		if abortErr != nil {

View on GitHub (pinned to d32a084675)