Billionmail/BillionMail · error
error saving footer config file: %v
Error message
error saving footer config file: %v
What it means
ModifyFooter fails at project.go:768 when os.WriteFile cannot persist the marshalled footer JSON to PRODUCT_CONFIG_PATH/<domain>/footer_config.json with mode 0644. The error is typically filesystem-level: the per-domain directory does not exist (os.WriteFile does not create parent dirs), the process lacks write permission, or the disk is full/read-only. The wrapped *fs.PathError is returned with the failing path.
Source
Thrown at core/internal/service/askai/project.go:768
if CopyrightText != "" {
config.CopyrightText = CopyrightText
}
if Disclaimer != "" {
config.Disclaimer = Disclaimer
}
if Text != "" {
config.Text = Text
}
config.UpdateTime = public.GetNowTime()
filename := fmt.Sprintf(PRODUCT_CONFIG_PATH+"/%s/footer_config.json", Domain)
data, err := json.Marshal(config)
if err != nil {
return fmt.Errorf("error marshalling footer config: %v", err)
}
err = os.WriteFile(filename, data, 0644)
if err != nil {
return fmt.Errorf("error saving footer config file: %v", err)
}
return nil
}
// GetPrompt retrieves the prompt configuration for a given domain from a JSON file.
// It reads the prompt configuration file and returns a PromptConfig struct.
// If the file does not exist or cannot be read, it returns an error.
func GetPrompt(Domain string) (PromptConfig, error) {
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.",
}View on GitHub (pinned to fc36c76c05)
Solutions
- Ensure the per-domain directory exists before writing: os.MkdirAll(filepath.Dir(filename), 0755).
- Check permissions/ownership of PRODUCT_CONFIG_PATH and the domain directory; chown/chmod so the running user can write.
- Verify the volume is mounted read-write and has free space (df -h, mount output).
- Read the wrapped *fs.PathError to confirm the exact failing path and errno (ENOENT vs EACCES vs ENOSPC).
Example fix
// before
filename := fmt.Sprintf(PRODUCT_CONFIG_PATH+"/%s/footer_config.json", Domain)
data, err := json.Marshal(config)
if err != nil { ... }
err = os.WriteFile(filename, data, 0644)
// after
filename := fmt.Sprintf(PRODUCT_CONFIG_PATH+"/%s/footer_config.json", Domain)
if err := os.MkdirAll(filepath.Dir(filename), 0755); err != nil {
return fmt.Errorf("error creating config dir: %v", err)
}
data, err := json.Marshal(config)
if err != nil { ... }
if err := os.WriteFile(filename, data, 0644); err != nil {
return fmt.Errorf("error saving footer config file: %v", err)
} Defensive patterns
Strategy: validation
Validate before calling
filename := fmt.Sprintf(PRODUCT_CONFIG_PATH+"/%s/footer_config.json", domain)
if err := os.MkdirAll(filepath.Dir(filename), 0755); err != nil {
return fmt.Errorf("config dir not writable: %v", err)
}
if f, err := os.OpenFile(filepath.Dir(filename), os.O_WRONLY, 0); err != nil {
return fmt.Errorf("no write permission on %s: %v", filepath.Dir(filename), err)
} else {
f.Close()
} Type guard
func isWritableDir(path string) bool {
info, err := os.Stat(path)
return err == nil && info.IsDir() && unix.Access(path, unix.W_OK) == nil
} Try / catch
if err := askai.ModifyFooter(domain, c, d, t); err != nil {
var perr *fs.PathError
if errors.As(err, &perr) {
switch {
case errors.Is(perr.Err, fs.ErrNotExist):
log.Printf("missing config dir %s — initialize it first", perr.Path)
case errors.Is(perr.Err, fs.ErrPermission):
log.Printf("cannot write %s — check ownership/permissions", perr.Path)
default:
log.Printf("write failed: %v", err)
}
}
return err
} Prevention
- Always os.MkdirAll the domain config directory before first write.
- Run the service as a user that owns PRODUCT_CONFIG_PATH, or chown the volume.
- Mount config volumes read-write in Docker Compose and monitor free disk space.
- Log the wrapped *fs.PathError path and errno, not just the message.
When it happens
Trigger: Calling ModifyFooter(domain, ...) when the directory PRODUCT_CONFIG_PATH/<domain> has not been created yet (no prior config was written for this domain), the directory/file is owned by another user with 0644 denying write, the volume is read-only or full, or Domain contains path-hostile characters.
Common situations: Fresh deployments or new domains where footer_config.json was never initialized and the domain directory was never created; running the app as a non-root container user against a config volume owned by root; a container restarted with a read-only mounted config volume; disk-full on the host.
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
- error saving default footer config file: %v
- error saving default prompt config file: %v
- error reading 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/b0f4034f5fe3ef4a.
Report an issue: GitHub.