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", filename) fails in WhisperTranscriber's bytes-based path (transcribing in-memory data). Identical semantics to the ElevenLabs case: the filename cannot be encoded into the Content-Disposition header (control characters, CR/LF, non-UTF-8 bytes) or the destination bytes.Buffer cannot allocate. The data itself is already in memory, so file I/O is not involved.

Source

Thrown at pkg/audio/asr/whisper_transcriber.go:107

func (t *WhisperTranscriber) TranscribeData(
	ctx context.Context,
	data []byte,
	filename string,
) (*TranscriptionResponse, error) {
	logger.InfoCF("voice", "Starting whisper transcription from memory", map[string]any{
		"bytes":    len(data),
		"filename": filename,
		"model":    t.modelID,
		"provider": t.providerName,
	})

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

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

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

	if err = writer.WriteField("model", t.modelID); err != nil {
		logger.ErrorCF("voice", "Failed to write whisper model field", map[string]any{"error": err})
		return nil, fmt.Errorf("failed to write model field: %w", err)
	}

	if err = writer.WriteField("response_format", "json"); err != nil {
		logger.ErrorCF("voice", "Failed to write whisper response_format field", map[string]any{"error": err})
		return nil, fmt.Errorf("failed to write response_format field: %w", err)
	}

	if err = writer.Close(); err != nil {

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Sanitize the filename: keep [A-Za-z0-9._-] plus a known extension.
  2. Log the filename with %q to reveal invisible characters.
  3. If filenames are clean, treat as memory pressure (see buffer errors).

Example fix

// before
part, err := writer.CreateFormFile("file", filename)

// after
var b strings.Builder
for _, r := range filename {
    if r >= 32 && r < 127 && r != '"' { b.WriteRune(r) }
}
safe := b.String()
if safe == "" { safe = "audio.wav" }
part, err := writer.CreateFormFile("file", safe)
Defensive patterns

Strategy: validation

Validate before calling

func validUploadName(name string) error {
    if !utf8.ValidString(name) || strings.ContainsAny(name, "\"\r\n\\;") {
        return fmt.Errorf("rejecting unsafe upload filename %q", name)
    }
    return nil
}

Type guard

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

Try / catch

if err := validUploadName(filename); err != nil { return err }
if _, err := whisper.TranscribeWithData(ctx, data, filename); err != nil {
    if strings.Contains(err.Error(), "failed to create form file") {
        return fmt.Errorf("filename %q rejected; rename and retry", filename)
    }
    return err
}

Prevention

When it happens

Trigger: Caller-supplied filename (used for both the form field and format detection elsewhere) contains \r, \n, quotes, or invalid UTF-8; memory exhaustion while buffering large audio.

Common situations: Filename built by string concatenation that accidentally embeds user text or a newline; blobs transcoded with machine-generated names containing odd bytes.

Related errors


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