sipeed/picoclaw · error

failed to create form file: %w

Error message

failed to create form file: %w

What it means

Thrown when multipart.Writer.CreateFormFile("file", filepath.Base(audioFilePath)) fails while building the ElevenLabs upload. CreateFormFile writes a part header to a bytes.Buffer, so it only fails if the filename cannot be encoded into the Content-Disposition header (invalid characters) or the buffer write fails. Practically a configuration-of-input error, not a runtime one.

Source

Thrown at pkg/audio/asr/elevenlabs_transcriber.go:74

	fileInfo, err := audioFile.Stat()
	if err != nil {
		logger.ErrorCF("voice", "Failed to get file info", map[string]any{"path": audioFilePath, "error": err})
		return nil, fmt.Errorf("failed to get file info: %w", err)
	}

	logger.DebugCF("voice", "Audio file details", map[string]any{
		"size_bytes": fileInfo.Size(),
		"file_name":  filepath.Base(audioFilePath),
	})

	var requestBody bytes.Buffer
	writer := multipart.NewWriter(&requestBody)

	part, err := writer.CreateFormFile("file", filepath.Base(audioFilePath))
	if err != nil {
		logger.ErrorCF("voice", "Failed to create form file", map[string]any{"error": err})
		return nil, fmt.Errorf("failed to create form file: %w", err)
	}

	if _, err = io.Copy(part, audioFile); err != nil {
		logger.ErrorCF("voice", "Failed to copy file content", map[string]any{"error": err})
		return nil, fmt.Errorf("failed to copy file content: %w", err)
	}

	if err = writer.WriteField("model_id", t.modelID); err != nil {
		return nil, fmt.Errorf("failed to write model_id field: %w", err)
	}

	if err = writer.Close(); err != nil {
		logger.ErrorCF("voice", "Failed to close multipart writer", map[string]any{"error": err})
		return nil, fmt.Errorf("failed to close multipart writer: %w", err)
	}

	url := t.apiBase + "/v1/speech-to-text"
	req, err := http.NewRequestWithContext(ctx, "POST", url, &requestBody)

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Sanitize the filename before transcription: strip control characters and keep an ASCII-safe base name.
  2. Reproduce by logging filepath.Base(audioFilePath) and checking for hidden \r/\n/non-UTF-8 bytes.
  3. If it happens without odd filenames, suspect memory pressure and profile the process.

Example fix

// before
part, err := writer.CreateFormFile("file", filepath.Base(audioFilePath))

// after (sanitize at the call site)
name := strings.Map(func(r rune) rune {
    if r < 32 || r == 127 || r == '"' { return -1 }
    return r
}, filepath.Base(audioFilePath))
part, err := writer.CreateFormFile("file", name)
Defensive patterns

Strategy: validation

Validate before calling

func safePartName(path string) (string, error) {
    name := filepath.Base(path)
    if !utf8.ValidString(name) || strings.ContainsAny(name, "\"\r\n\\") {
        return "", fmt.Errorf("unsafe audio filename %q", name)
    }
    return name, nil
}

Type guard

func hasControlChars(s string) bool {
    return strings.IndexFunc(s, func(r rune) bool { return r < 32 || r == 127 }) >= 0
}

Try / catch

if _, err := el.Transcribe(ctx, path); err != nil && strings.Contains(err.Error(), "failed to create form file") {
    return fmt.Errorf("audio filename rejected (%q): rename the file and retry", filepath.Base(path))
}

Prevention

When it happens

Trigger: The base filename contains characters the multipart header writer rejects, e.g. CR/LF, quotes, or non-UTF-8 bytes; otherwise only an out-of-memory condition on bytes.Buffer triggers it.

Common situations: Recordings named with embedded newlines or control characters (buggy recorder using a timestamp format with \n); filenames from another OS with invalid byte sequences.

Related errors


AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15). Data as JSON: /api/errors/7289d05be78361cc. Report an issue: GitHub.