sipeed/picoclaw · error

create temp file: %w

Error message

create temp file: %w

What it means

os.CreateTemp(mediaDir, msgID+"-*"+ext) failed while writing the downloaded inbound media (media.go:331-333). msgID arrives from the WeCom payload and is used verbatim as the pattern prefix, so a msgID containing '/' (or otherwise hostile bytes) makes CreateTemp target a nonexistent subdirectory. The %w wraps *fs.PathError: EACCES, ENOSPC, EMFILE (fd limit), EEXIST races, ENOENT when the spool dir vanished between MkdirAll and CreateTemp.

Source

Thrown at pkg/channels/wecom/media.go:333

	filename, contentType := detectWeComMediaMetadata(
		data,
		msgID+fallbackExt,
		resp.Header.Get("Content-Type"),
		resourceURL,
		resp.Header.Get("Content-Disposition"),
	)
	ext := filepath.Ext(filename)
	if ext == "" {
		ext = inferMediaExt(contentType, fallbackExt)
	}
	mediaDir := filepath.Join(os.TempDir(), "picoclaw_media")
	if mkdirErr := os.MkdirAll(mediaDir, 0o700); mkdirErr != nil {
		return "", fmt.Errorf("mkdir media dir: %w", mkdirErr)
	}
	tmpFile, err := os.CreateTemp(mediaDir, msgID+"-*"+ext)
	if err != nil {
		return "", fmt.Errorf("create temp file: %w", err)
	}
	tmpPath := tmpFile.Name()
	if _, writeErr := tmpFile.Write(data); writeErr != nil {
		_ = tmpFile.Close()
		_ = os.Remove(tmpPath)
		return "", fmt.Errorf("write temp file: %w", writeErr)
	}
	if closeErr := tmpFile.Close(); closeErr != nil {
		_ = os.Remove(tmpPath)
		return "", fmt.Errorf("close temp file: %w", closeErr)
	}

	ref, err := store.Store(tmpPath, media.MediaMeta{
		Filename:      filename,
		ContentType:   contentType,
		Source:        "wecom",
		CleanupPolicy: media.CleanupPolicyDeleteOnCleanup,
	}, scope)

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Sanitize the prefix: pattern := filepath.Base(msgID) + "-*" + ext so separators can never reach CreateTemp
  2. If EMFILE: raise ulimit -n / systemd LimitNOFILE and find the fd leak
  3. If ENOSPC/inode exhaustion: df -h and df -i the temp fs, clean or enlarge
  4. If a race with cleanup: serialize spool-dir creation/cleanup per process, or tolerate ENOENT with one MkdirAll+retry

Example fix

// before: raw platform msgID used as CreateTemp prefix
tmpFile, err := os.CreateTemp(mediaDir, msgID+"-*"+ext)

// after: sanitize to a single path element first
prefix := filepath.Base(msgID)
if prefix == "." || prefix == "/" || prefix == "" {
    prefix = "media"
}
tmpFile, err := os.CreateTemp(mediaDir, prefix+"-*"+ext)
Defensive patterns

Strategy: validation

Validate before calling

// precheck the pieces CreateTemp needs
func canCreateTemp(prefix string) error {
    if filepath.Base(prefix) != prefix || strings.ContainsAny(prefix, `/`) {
        return fmt.Errorf("unsafe temp prefix %q", prefix)
    }
    var stat unix.Statfs_t
    if err := unix.Statfs(filepath.Join(os.TempDir(), "picoclaw_media"), &stat); err != nil {
        return err
    }
    if stat.Ffree < 100 { // inode headroom
        return fmt.Errorf("temp fs nearly out of inodes")
    }
    return nil
}

Prevention

When it happens

Trigger: msgID with path separators or illegal bytes; inode or fd exhaustion on a busy host; the spool dir deleted by a concurrent cleanup between MkdirAll and CreateTemp; disk full at file creation time.

Common situations: Long-running hosts leaking fds (EMFILE after days of uptime); /tmp on a small tmpfs out of inodes; parallel test/process runs racing to clean picoclaw_media; malformed upstream msgIDs after gateway version changes.

Related errors


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