{"record":{"id":"b0f4034f5fe3ef4a","repo":"Billionmail/BillionMail","slug":"error-saving-footer-config-file-v","errorCode":null,"errorMessage":"error saving footer config file: %v","messagePattern":"error saving footer config file: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"core/internal/service/askai/project.go","lineNumber":768,"sourceCode":"\tif CopyrightText != \"\" {\n\t\tconfig.CopyrightText = CopyrightText\n\t}\n\tif Disclaimer != \"\" {\n\t\tconfig.Disclaimer = Disclaimer\n\t}\n\tif Text != \"\" {\n\t\tconfig.Text = Text\n\t}\n\n\tconfig.UpdateTime = public.GetNowTime()\n\tfilename := fmt.Sprintf(PRODUCT_CONFIG_PATH+\"/%s/footer_config.json\", Domain)\n\tdata, err := json.Marshal(config)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error marshalling footer config: %v\", err)\n\t}\n\terr = os.WriteFile(filename, data, 0644)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error saving footer config file: %v\", err)\n\t}\n\treturn nil\n}\n\n// GetPrompt retrieves the prompt configuration for a given domain from a JSON file.\n// It reads the prompt configuration file and returns a PromptConfig struct.\n// If the file does not exist or cannot be read, it returns an error.\nfunc GetPrompt(Domain string) (PromptConfig, error) {\n\tfilename := fmt.Sprintf(PRODUCT_CONFIG_PATH+\"/%s/prompt_config.json\", Domain)\n\tif !public.FileExists(filename) {\n\t\t// If the prompt config file does not exist, return a default prompt config\n\t\t// This allows the system to handle cases where the prompt configuration has not been set up\n\t\t// and avoids errors when trying to read a non-existent file.\n\t\t// It also allows the user to create a new prompt configuration without needing to handle file\n\t\t// not found errors.\n\t\tdefaultPromptConfig := PromptConfig{\n\t\t\tPrompt: \"This is a default prompt. Please customize it.\",\n\t\t}","sourceCodeStart":750,"sourceCodeEnd":786,"githubUrl":"https://github.com/Billionmail/BillionMail/blob/fc36c76c050c3775c5e899faf7403cf0262d2744/core/internal/service/askai/project.go#L750-L786","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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)."],"exampleFix":"// before\nfilename := fmt.Sprintf(PRODUCT_CONFIG_PATH+\"/%s/footer_config.json\", Domain)\ndata, err := json.Marshal(config)\nif err != nil { ... }\nerr = os.WriteFile(filename, data, 0644)\n// after\nfilename := fmt.Sprintf(PRODUCT_CONFIG_PATH+\"/%s/footer_config.json\", Domain)\nif err := os.MkdirAll(filepath.Dir(filename), 0755); err != nil {\n    return fmt.Errorf(\"error creating config dir: %v\", err)\n}\ndata, err := json.Marshal(config)\nif err != nil { ... }\nif err := os.WriteFile(filename, data, 0644); err != nil {\n    return fmt.Errorf(\"error saving footer config file: %v\", err)\n}","handlingStrategy":"validation","validationCode":"filename := fmt.Sprintf(PRODUCT_CONFIG_PATH+\"/%s/footer_config.json\", domain)\nif err := os.MkdirAll(filepath.Dir(filename), 0755); err != nil {\n    return fmt.Errorf(\"config dir not writable: %v\", err)\n}\nif f, err := os.OpenFile(filepath.Dir(filename), os.O_WRONLY, 0); err != nil {\n    return fmt.Errorf(\"no write permission on %s: %v\", filepath.Dir(filename), err)\n} else {\n    f.Close()\n}","typeGuard":"func isWritableDir(path string) bool {\n    info, err := os.Stat(path)\n    return err == nil && info.IsDir() && unix.Access(path, unix.W_OK) == nil\n}","tryCatchPattern":"if err := askai.ModifyFooter(domain, c, d, t); err != nil {\n    var perr *fs.PathError\n    if errors.As(err, &perr) {\n        switch {\n        case errors.Is(perr.Err, fs.ErrNotExist):\n            log.Printf(\"missing config dir %s — initialize it first\", perr.Path)\n        case errors.Is(perr.Err, fs.ErrPermission):\n            log.Printf(\"cannot write %s — check ownership/permissions\", perr.Path)\n        default:\n            log.Printf(\"write failed: %v\", err)\n        }\n    }\n    return err\n}","preventionTips":["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."],"tags":["go","filesystem","permissions","config"],"backgroundTag":"file-write-permission-denied","analyzedSha":"fc36c76c050c3775c5e899faf7403cf0262d2744","analyzedAt":"2026-09-05T21:28:54.019Z","contentChangedAt":"2026-09-05T21:28:54.019Z","schemaVersion":2},"datasetVersion":"2026-09-12T22:17:10.623Z"}