Billionmail/BillionMail · error

upload video: %w

Error message

upload video: %w

What it means

UploadVideoAssets uploads the video then the thumbnail via UploadFile and wraps the first failure with 'upload video: %w', so callers get a labeled pipeline-stage error whose chain still contains the underlying 'open file for upload' or 'r2 upload' cause.

Source

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

		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 {
		return "", "", fmt.Errorf("upload thumbnail: %w", err)
	}

	return videoResult.PublicURL, thumbResult.PublicURL, nil
}

// detectContentType returns the MIME type based on file extension.
func detectContentType(filename string) string {
	ext := strings.ToLower(filepath.Ext(filename))
	switch ext {
	case ".mp4":
		return "video/mp4"
	case ".webm":
		return "video/webm"

View on GitHub (pinned to fc36c76c05)

Solutions

  1. Inspect errors.Is/As down the wrapped chain to find whether it is a file-open failure (fix the video path/generation) or an R2 failure (fix config/network)
  2. Ensure RunPipeline's video generation step succeeds and cfg.OutputPath matches the videoPath passed here before uploading
  3. Validate both videoPath and thumbnailPath with os.Stat before calling UploadVideoAssets
  4. Verify R2Config credentials/endpoint if uploads fail uniformly
  5. Use errors.Is(err, context.Canceled) to distinguish intentional shutdowns from real failures

Example fix

// before
videoResult, err := UploadFile(ctx, cfg, videoPath, contactID)
if err != nil {
    return "", "", fmt.Errorf("upload video: %w", err)
}
// after
if _, err := os.Stat(videoPath); err != nil {
    return "", "", fmt.Errorf("video file missing before upload: %s", videoPath)
}
videoResult, err := UploadFile(ctx, cfg, videoPath, contactID)
if err != nil {
    return "", "", fmt.Errorf("upload video: %w", err)
}
Defensive patterns

Strategy: validation

Validate before calling

for _, p := range []string{videoPath, thumbnailPath} {
    if _, err := os.Stat(p); err != nil {
        return fmt.Errorf("asset missing before upload: %s", p)
    }
}
if err := validateR2Config(cfg); err != nil {
    return err
}

Try / catch

vURL, tURL, err := video_gen.UploadVideoAssets(ctx, cfg, videoPath, thumbPath, contactID)
if err != nil {
    var pathErr *os.PathError
    if errors.As(err, &pathErr) {
        log.Errorf("upload stage failed on local file: %v", err) // chain shows which asset
    } else {
        return fmt.Errorf("pipeline upload stage: %w", err)
    }
}

Prevention

When it happens

Trigger: UploadFile(ctx, cfg, videoPath, contactID) fails because the generated video file is missing on disk (TestUploadVideoAssets_FirstFileMissing) or the R2 PutObject call errors; the wrapped chain is then labeled as the video-upload stage.

Common situations: Video generation step failed or wrote to a different output path so the file never exists; R2 credentials/endpoint misconfigured for every upload; worker context cancelled during a long video upload.

Related errors


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