Billionmail/BillionMail · error

error saving default prompt config file: %v

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.

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)

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.