Billionmail/BillionMail · error

r2 upload: %w

Error message

r2 upload: %w

What it means

After opening the local file, UploadFile calls the AWS S3-compatible PutObject client against Cloudflare R2; any client-side failure is wrapped as 'r2 upload: %w'. This is the S3 SDK's operation error (e.g. *smithy.OperationError or *types.*Error) carrying the transport-level cause.

Source

Thrown at core/internal/service/video_gen/upload.go:97

	f, err := os.Open(localPath)
	if err != nil {
		return nil, fmt.Errorf("open file for upload: %w", err)
	}
	defer f.Close()

	filename := filepath.Base(localPath)
	key := BuildR2ObjectKey(contactID, filename)
	contentType := detectContentType(filename)

	client := NewR2Client(cfg)
	_, err = client.PutObject(ctx, &s3.PutObjectInput{
		Bucket:      aws.String(cfg.BucketName),
		Key:         aws.String(key),
		Body:        f,
		ContentType: aws.String(contentType),
	})
	if err != nil {
		return nil, fmt.Errorf("r2 upload: %w", err)
	}

	return &UploadResult{
		Key:       key,
		PublicURL: BuildPublicURL(cfg, key),
	}, nil
}

// UploadVideoAssets uploads the video and thumbnail to R2.
// Returns public URLs for both.
func UploadVideoAssets(ctx context.Context, cfg R2Config, videoPath, thumbnailPath, contactID string) (videoURL, thumbURL string, err error) {
	videoResult, err := UploadFile(ctx, cfg, videoPath, contactID)
	if err != nil {
		return "", "", fmt.Errorf("upload video: %w", err)
	}

	thumbResult, err := UploadFile(ctx, cfg, thumbnailPath, contactID)
	if err != nil {

View on GitHub (pinned to fc36c76c05)

Solutions

  1. Log the full wrapped error chain (errors.Unwrap / %+v) to get the S3 error code, then correct the specific cause (endpoint, credentials, or bucket)
  2. Verify cfg in R2Config: AccountID, AccessKeyID, SecretAccessKey and BucketName against the Cloudflare dashboard and a tool like 'aws s3 cp' with the R2 endpoint
  3. Confirm the R2 API token has write permission on the bucket
  4. Check container egress networking/DNS to <account>.r2.cloudflarestorage.com
  5. If transient (timeout/5xx), retry with backoff before failing the pipeline

Example fix

// before
_, err := client.PutObject(ctx, &s3.PutObjectInput{...})
if err != nil {
    return nil, fmt.Errorf("r2 upload: %w", err)
}
// after
var ae smithy.APIError
_, err := client.PutObject(ctx, &s3.PutObjectInput{...})
if err != nil {
    if errors.As(err, &ae) && ae.ErrorCode() == "NoSuchBucket" {
        return nil, fmt.Errorf("r2 bucket %q does not exist", cfg.BucketName)
    }
    return nil, fmt.Errorf("r2 upload: %w", err)
}
Defensive patterns

Strategy: try-catch

Validate before calling

if cfg.BucketName == "" || cfg.AccessKeyID == "" || cfg.SecretAccessKey == "" || cfg.AccountID == "" {
    return fmt.Errorf("incomplete R2 config")
}
endpoint := fmt.Sprintf("https://%s.r2.cloudflarestorage.com", cfg.AccountID)
if _, err := url.Parse(endpoint); err != nil {
    return fmt.Errorf("invalid R2 endpoint: %w", err)
}

Try / catch

res, err := video_gen.UploadFile(ctx, cfg, localPath, contactID)
if err != nil {
    var oe *smithy.OperationError
    if errors.As(err, &oe) {
        log.Errorf("r2 upload failed: %v (%v)", oe, oe.Unwrap())
        if isRetryable(oe) {
            // retry with backoff
        }
    }
}

Prevention

When it happens

Trigger: PutObject returns an error: unreachable/bad AccountID endpoint, invalid or missing R2 access/secret keys (403 InvalidAccessKeyId/SignatureDoesNotMatch), nonexistent bucket name, network timeouts, or the reader failing mid-stream.

Common situations: Wrong endpoint built from a mistyped Cloudflare account ID; rotated or scope-less R2 API tokens; bucket name mismatch between config and Cloudflare dashboard; no outbound network/DNS from the container; request context cancelled (TestUploadFile_CancelledContext).

Related errors


AI-assisted analysis of Billionmail/BillionMail@fc36c76c05 (2026-09-05). Data as JSON: /api/errors/40144a49f393f74e. Report an issue: GitHub.