Billionmail/BillionMail · error

error reading prompt config file: %v

Error message

error reading prompt config file: %v

What it means

GetPrompt fails at project.go:801 when os.ReadFile cannot read the existing PRODUCT_CONFIG_PATH/<domain>/prompt_config.json. Since the code already checks public.FileExists, ENOENT here usually means a race (file deleted between the check and the read) or a broken symlink; other causes are permission denial on the file or directory, or an I/O error. The wrapped *fs.PathError names the path and errno.

Source

Thrown at core/internal/service/askai/project.go:801

		// not found errors.
		defaultPromptConfig := PromptConfig{
			Prompt: "This is a default prompt. Please customize it.",
		}
		defaultPromptConfig.UpdateTime = public.GetNowTime()
		defaultPromptJson, err := json.MarshalIndent(defaultPromptConfig, "", "  ")

		if err != nil {
			return PromptConfig{}, fmt.Errorf("error marshalling default prompt config: %v", err)
		}
		err = os.WriteFile(filename, defaultPromptJson, 0644)
		if err != nil {
			return PromptConfig{}, fmt.Errorf("error saving default prompt config file: %v", err)
		}
		return defaultPromptConfig, nil
	}
	data, err := os.ReadFile(filename)
	if err != nil {
		return PromptConfig{}, fmt.Errorf("error reading prompt config file: %v", err)
	}

	var config PromptConfig
	err = json.Unmarshal(data, &config)
	if err != nil {
		return PromptConfig{}, fmt.Errorf("error unmarshalling prompt config: %v", err)
	}
	return config, nil
}

// ModifyPrompt updates the prompt configuration for a given domain.
// It reads the existing prompt configuration, modifies the specified fields, and saves the updated configuration back to the file.
// If the prompt is empty, it will not be updated.
func ModifyPrompt(Domain string, Prompt string) error {
	config, err := GetPrompt(Domain)
	if err != nil {
		return fmt.Errorf("error getting prompt config: %v", err)
	}

View on GitHub (pinned to fc36c76c05)

Solutions

  1. Inspect the wrapped *fs.PathError errno: ENOENT → race/dead symlink, EACCES → permissions, EIO → storage.
  2. Check file and directory permissions so the service user can read prompt_config.json and traverse the domain dir.
  3. Replace the FileExists check + read with a single read and treat os.IsNotExist as the default-config case, eliminating the race.
  4. Ensure no cron/deployment job deletes config files while the service runs.

Example fix

// before
if !public.FileExists(filename) { ...default config... }
data, err := os.ReadFile(filename)
if err != nil {
    return PromptConfig{}, fmt.Errorf("error reading prompt config file: %v", err)
}
// after
data, err := os.ReadFile(filename)
if err != nil {
    if os.IsNotExist(err) {
        ...create and return default config...
    }
    return PromptConfig{}, fmt.Errorf("error reading prompt config file: %v", err)
}
Defensive patterns

Strategy: fallback

Validate before calling

filename := fmt.Sprintf(PRODUCT_CONFIG_PATH+"/%s/prompt_config.json", domain)
if info, err := os.Stat(filename); err != nil || info.IsDir() {
    // file missing or not a regular file — prepare to use/create default config
}
if f, err := os.Open(filename); err != nil {
    return fmt.Errorf("file unreadable: %v", err)
} else {
    f.Close()
}

Type guard

func promptConfigReadable(path string) bool {
    f, err := os.Open(path)
    if err != nil {
        return false
    }
    f.Close()
    return true
}

Try / catch

cfg, err := askai.GetPrompt(domain)
if err != nil {
    var perr *fs.PathError
    if errors.As(err, &perr) && errors.Is(perr.Err, fs.ErrNotExist) {
        // fall back to defaults instead of failing
        cfg = askai.DefaultPromptConfig()
    } else if errors.As(err, &perr) && errors.Is(perr.Err, fs.ErrPermission) {
        log.Printf("cannot read %s — fix ownership/permissions", perr.Path)
        return err
    } else {
        return err
    }
}

Prevention

When it happens

Trigger: Calling GetPrompt(domain) (or ModifyPrompt) when prompt_config.json exists in the FileExists check but becomes unreadable: deleted concurrently by another process, permission bits (0644 in a dir the service user cannot traverse), symlink pointing nowhere, or EIO on storage.

Common situations: Concurrent deployments or cleanup jobs removing config files while the app reads them; config dir copied/rsynced with wrong ownership; Docker volume permission mismatch after image user change; corrupted/failed mount.

Understand the failure class

Background: "failed to read file", EACCES, ENOENT and "could not read <path>" errors: when a program can't read a file from disk — this error's family across 49 libraries.

Related errors


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