Billionmail/BillionMail · error

create output file: %w

Error message

create output file: %w

What it means

After a successful TTS response, TextToSpeech creates the destination file at filepath.Join(cfg.OutputDir, filename) with os.Create. If file creation fails — bad directory, permission denied, invalid filename characters, name too long, or disk full — the OS error is wrapped as 'create output file'.

Source

Thrown at core/internal/service/video_gen/voice.go:201

		return "", err
	}
	httpReq = httpReq.WithContext(ctx)

	resp, err := cfg.doHTTP(httpReq)
	if err != nil {
		return "", fmt.Errorf("cartesia TTS API call: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		return "", fmt.Errorf("cartesia TTS API error %d: %s", resp.StatusCode, string(body))
	}

	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 audio data: %w", err)
	}

	return outPath, nil
}

View on GitHub (pinned to fc36c76c05)

Solutions

  1. Log the full outPath and wrapped OS error (os.PathError reveals the cause).
  2. Sanitize filename: strip path separators and illegal characters before joining.
  3. Ensure OutputDir is writable by the service user (chown/chmod or run as correct user).
  4. Check free disk space (df) and filesystem mount options (ro?).
  5. Use a collision-safe name (filepath.Base the input, add unique suffix).

Example fix

// before
outPath := filepath.Join(cfg.OutputDir, filename)
f, err := os.Create(outPath)
if err != nil {
    return "", fmt.Errorf("create output file: %w", err)
}
// after
safe := strings.Map(func(r rune) rune {
    if r == '/' || r == '\\' || r == 0 || unicode.IsControl(r) { return -1 }
    return r
}, filename)
outPath := filepath.Join(cfg.OutputDir, safe)
f, err := os.Create(outPath)
if err != nil {
    return "", fmt.Errorf("create output file %s: %w", outPath, err)
}
Defensive patterns

Strategy: validation

Validate before calling

func sanitizeFilename(name string) (string, error) {
    name = filepath.Base(name)
    name = strings.Map(func(r rune) rune {
        if r == '/' || r == '\\' || r == 0 || unicode.IsControl(r) { return -1 }
        return r
    }, name)
    if name == "" || name == "." || len(name) > 255 {
        return "", fmt.Errorf("invalid filename %q", name)
    }
    return name, nil
}
safe, err := sanitizeFilename(filename)
if err != nil {
    return err
}
// also ensure dir is writable (see error 492 validation)

Try / catch

path, err := video_gen.TextToSpeech(ctx, cfg, voiceID, transcript, filename)
if err != nil {
    if strings.Contains(err.Error(), "create output file") {
        var pathErr *os.PathError
        if errors.As(err, &pathErr) {
            log.Printf("cannot create %s: %v", pathErr.Path, pathErr.Err)
        }
        return err
    }
    return err
}

Prevention

When it happens

Trigger: OutputDir exists but is not writable, filename contains '/' or illegal characters (e.g. derived from user input), path exceeds NAME_MAX, or the filesystem is full/read-only.

Common situations: Filenames built from user-supplied transcripts/timestamps with slashes or control chars, running as non-root against root-owned output dir, small tmpfs filling up during bulk generation.

Related errors


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