Billionmail/BillionMail · error

error saving default prompt config file

Error message

error saving default prompt config file: %v

What it means

GetPrompt fails at project.go:795 when os.WriteFile cannot create prompt_config.json (mode 0644) in PRODUCT_CONFIG_PATH/<domain>/ while initializing the default prompt config for a domain that has no config file yet. os.WriteFile does not create parent directories, so a missing domain directory is the dominant cause; permission, read-only-filesystem, and disk-space problems produce the same error. The wrapped *fs.PathError identifies the exact path and errno.

Solutions

  1. Create the directory before writing: os.MkdirAll(filepath.Dir(filename), 0755) in the default-config branch.
  2. Fix permissions/ownership on PRODUCT_CONFIG_PATH so the service user can create files.
  3. Confirm the config volume is mounted read-write and has free space.
  4. Inspect the wrapped *fs.PathError errno (ENOENT/EACCES/ENOSPC/EROFS) to pick the right fix.

Example fix

// before
defaultPromptJson, err := json.MarshalIndent(defaultPromptConfig, "", "  ")
if err != nil { ... }
err = os.WriteFile(filename, defaultPromptJson, 0644)
// after
defaultPromptJson, err := json.MarshalIndent(defaultPromptConfig, "", "  ")
if err != nil { ... }
if err := os.MkdirAll(filepath.Dir(filename), 0755); err != nil {
    return PromptConfig{}, fmt.Errorf("error creating config dir: %v", err)
}
if err := os.WriteFile(filename, defaultPromptJson, 0644); err != nil {
    return PromptConfig{}, fmt.Errorf("error saving default prompt config file: %v", err)
}
Defensive patterns

Strategy: validation

Validate before calling

filename := fmt.Sprintf(PRODUCT_CONFIG_PATH+"/%s/prompt_config.json", domain)
if err := os.MkdirAll(filepath.Dir(filename), 0755); err != nil {
    return fmt.Errorf("cannot create config dir: %v", err)
}
probe := filepath.Join(filepath.Dir(filename), ".write_probe")
if err := os.WriteFile(probe, nil, 0644); err != nil {
    return fmt.Errorf("dir not writable: %v", err)
}
os.Remove(probe)

Type guard

func canCreateIn(dir string) bool {
    probe := filepath.Join(dir, ".probe")
    if err := os.WriteFile(probe, nil, 0644); err != nil {
        return false
    }
    os.Remove(probe)
    return true
}

Try / catch

cfg, err := askai.GetPrompt(domain)
if err != nil {
    var perr *fs.PathError
    if errors.As(err, &perr) {
        switch {
        case errors.Is(perr.Err, fs.ErrNotExist):
            log.Printf("config dir %s missing — run MkdirAll during domain setup", perr.Path)
        case errors.Is(perr.Err, fs.ErrPermission):
            log.Printf("permission denied on %s — fix volume ownership", perr.Path)
        default:
            log.Printf("default prompt write failed: %v", err)
        }
    }
    return err
}

Prevention

When it happens

Trigger: First call to GetPrompt(domain) (or ModifyPrompt which calls it) for a domain whose directory PRODUCT_CONFIG_PATH/<domain> was never created, when the process lacks write permission on that path, or when the config volume is read-only/full.

Common situations: Newly onboarded domain where no other config file has been written yet (nothing created the domain dir); app running as unprivileged container user while config dir is root-owned; docker-compose config volume mounted read-only; host disk full.

Understand the failure class

Background: "Permission denied" / "Failed to write" file errors: why a library can't write its files to disk (EACCES, EPERM, ENOSPC) and how to fix them — this error's family across 43 libraries.

Related errors


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

Appendix: source

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

	filename := fmt.Sprintf(PRODUCT_CONFIG_PATH+"/%s/prompt_config.json", Domain)
	if !public.FileExists(filename) {
		// If the prompt config file does not exist, return a default prompt config
		// This allows the system to handle cases where the prompt configuration has not been set up
		// and avoids errors when trying to read a non-existent file.
		// It also allows the user to create a new prompt configuration without needing to handle file
		// 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.

View on GitHub (pinned to fc36c76c05)