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

  1. Ensure the per-domain directory exists before writing: os.MkdirAll(filepath.Dir(filename), 0755).
  2. Check permissions/ownership of PRODUCT_CONFIG_PATH and the domain directory; chown/chmod so the running user can write.
  3. Verify the volume is mounted read-write and has free space (df -h, mount output).
  4. 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

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


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