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
- Inspect the wrapped *fs.PathError errno: ENOENT → race/dead symlink, EACCES → permissions, EIO → storage.
- Check file and directory permissions so the service user can read prompt_config.json and traverse the domain dir.
- Replace the FileExists check + read with a single read and treat os.IsNotExist as the default-config case, eliminating the race.
- 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
- Replace exists-check + read with a single os.ReadFile and handle os.IsNotExist as the default-config path, removing the race window.
- Fix file permissions/ownership after copying or syncing config directories.
- Prevent cleanup or deploy jobs from deleting config files while the service runs.
- Fall back to defaults on ENOENT only; surface EACCES/EIO as real errors.
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
- error saving default footer config file: %v
- error saving footer config file: %v
- error saving default prompt config file: %v
- project configuration file does not exist: %s
- error reading project configuration file: %v
AI-assisted analysis of Billionmail/BillionMail@fc36c76c05 (2026-09-05).
Data as JSON: /api/errors/9f18ae3440e33ee6.
Report an issue: GitHub.