Billionmail/BillionMail · error

create output file: %w

Error message

create output file: %w

What it means

os.Create failed when creating the destination file at filepath.Join(cfg.OutputDir, filename). The output directory is created beforehand via os.MkdirAll, so this usually means a filesystem-level problem: no write permission, disk full, OutputDir pointing at a file or read-only mount, or a filename containing path separators / illegal characters.

Source

Thrown at core/internal/service/video_gen/lipsync.go:185

	req, err := http.NewRequestWithContext(ctx, "GET", videoURL, nil)
	if err != nil {
		return "", fmt.Errorf("create download request: %w", err)
	}

	resp, err := cfg.doHTTP(req)
	if err != nil {
		return "", fmt.Errorf("download lipsync video: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return "", fmt.Errorf("download error %d", resp.StatusCode)
	}

	outPath := filepath.Join(cfg.OutputDir, filename)
	f, err := os.Create(outPath)
	if err != nil {
		return "", fmt.Errorf("create output file: %w", err)
	}
	defer f.Close()

	if _, err := io.Copy(f, resp.Body); err != nil {
		return "", fmt.Errorf("write video data: %w", err)
	}

	return outPath, nil
}

View on GitHub (pinned to fc36c76c05)

Solutions

  1. Check free disk space (df -h) — ENOSPC is the most common cause in video pipelines.
  2. Verify cfg.OutputDir exists as a directory and is writable by the process user; fix volume mount permissions.
  3. Sanitize filename: strip path separators (filepath.Base) and validate it before calling.
  4. Confirm nothing pre-created outPath as a directory; remove the conflicting path.
  5. If using a read-only container filesystem, mount a writable volume for OutputDir.

Example fix

// before
dlPath, lipErr := DownloadLipSyncVideo(ctx, lipCfg, lipVideoURL, "lipsync.mp4")
// after
safeName := filepath.Base("lipsync.mp4") // strip any path components
if err := os.MkdirAll(lipCfg.OutputDir, 0755); err != nil { /* surface early */ }
dlPath, lipErr := DownloadLipSyncVideo(ctx, lipCfg, lipVideoURL, safeName)
Defensive patterns

Strategy: validation

Validate before calling

// before calling DownloadLipSyncVideo
if info, err := os.Stat(cfg.OutputDir); err != nil {
	if err := os.MkdirAll(cfg.OutputDir, 0755); err != nil {
		return fmt.Errorf("output dir unusable: %w", err)
	}
} else if !info.IsDir() {
	return fmt.Errorf("output path %s is not a directory", cfg.OutputDir)
}
safe := filepath.Base(filename)
if safe == "." || safe == "/" || strings.ContainsAny(safe, "\x00") {
	return fmt.Errorf("invalid filename: %q", filename)
}

Type guard

func isFSError(err error) bool {
	return errors.Is(err, os.ErrPermission) || errors.Is(err, syscall.ENOSPC) || errors.Is(err, os.ErrExist)
}

Try / catch

path, err := DownloadLipSyncVideo(ctx, cfg, videoURL, filename)
if err != nil && strings.Contains(err.Error(), "create output file") {
	// check disk space, permissions, and filename validity before retrying
	return fmt.Errorf("cannot write to %s: %w", cfg.OutputDir, err)
}

Prevention

When it happens

Trigger: os.Create(outPath) returns an error: OutputDir is a read-only volume, the disk is full, 'filename' contains '/' (e.g. derived from a job or API value) making a nonexistent subdirectory, or the path collides with an existing directory.

Common situations: Running the container with a read-only root filesystem and tmpDir defaulting to /tmp/video_gen_<id> that got pre-created as something else; disk quota exhausted after storing many videos; filename built from untrusted input containing slashes; permission mismatch between the process user (e.g. non-root) and the mounted output volume.

Understand the failure class

Background: "Permission denied" / "Failed to write" file errors: why a library can't write its files to disk (EACCES, EPERM, ENOSPC) and how to fix them — this error's family across 43 libraries.

Related errors


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