{"record":{"id":"9e36c6d2329f2eba","repo":"Billionmail/BillionMail","slug":"error-reading-footer-config-file-v","errorCode":null,"errorMessage":"error reading footer config file: %v","messagePattern":"error reading footer config file: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"core/internal/service/askai/project.go","lineNumber":729,"sourceCode":"\t\tdefaultFooterConfig := FooterConfig{\n\t\t\tCopyrightText: \"© 2023 Your Company. All rights reserved.\",\n\t\t\tDisclaimer:    \"This is a sample disclaimer.\",\n\t\t}\n\t\tdefaultFooterConfig.UpdateTime = public.GetNowTime()\n\t\tdefaultFooterJson, err := json.MarshalIndent(defaultFooterConfig, \"\", \"  \")\n\t\tif err != nil {\n\t\t\treturn FooterConfig{}, fmt.Errorf(\"error marshalling default footer config: %v\", err)\n\t\t}\n\t\terr = os.WriteFile(filename, defaultFooterJson, 0644)\n\t\tif err != nil {\n\t\t\treturn FooterConfig{}, fmt.Errorf(\"error saving default footer config file: %v\", err)\n\t\t}\n\t\t// Return the default footer config if the file does not exist\n\t\treturn defaultFooterConfig, nil\n\t}\n\tdata, err := os.ReadFile(filename)\n\tif err != nil {\n\t\treturn FooterConfig{}, fmt.Errorf(\"error reading footer config file: %v\", err)\n\t}\n\n\tvar config FooterConfig\n\terr = json.Unmarshal(data, &config)\n\tif err != nil {\n\t\treturn FooterConfig{}, fmt.Errorf(\"error unmarshalling footer config: %v\", err)\n\t}\n\treturn config, nil\n}\n\n// ModifyFooter updates the footer configuration for a given domain.\n// It reads the existing footer configuration, modifies the specified fields, and saves the updated configuration back to the file.\n// If any field is empty, it will not be updated.\nfunc ModifyFooter(Domain string, CopyrightText, Disclaimer string, Text string) error {\n\tconfig, err := GetFooter(Domain)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error getting footer config: %v\", err)\n\t}","sourceCodeStart":711,"sourceCodeEnd":747,"githubUrl":"https://github.com/Billionmail/BillionMail/blob/fc36c76c050c3775c5e899faf7403cf0262d2744/core/internal/service/askai/project.go#L711-L747","documentation":"When footer_config.json exists, GetFooter reads it with os.ReadFile and wraps any read failure. Unlike the missing-file case (which auto-provisions a default), this fires when the file exists but cannot be read — typically permission problems, the path being a directory, or an I/O error occurring between the FileExists check and the read (race/deleted file, broken symlink).","triggerScenarios":"Calling GetFooter (or ModifyFooter/GetFooterPrompt/AutoGetProjectInfo that call it) when footer_config.json exists but is unreadable: owned by another user with restrictive modes, path is a directory named footer_config.json, dangling symlink, or the file is deleted between the FileExists check and ReadFile.","commonSituations":"Operator copied footer_config.json in as root with 0600 while the app runs unprivileged; a mount point or directory accidentally named footer_config.json; backup/sync tool replaced the file with a broken symlink.","solutions":["Check the wrapped *PathError: 'permission denied' → fix ownership (chown service-user) and mode (chmod 0644) of footer_config.json.","Verify the path is a regular file (ls -la); if it is a directory or symlink, replace it with a valid config file.","If 'no such file or directory' appears despite existence, the file vanished between check and read — replace the FileExists+ReadFile pattern with a single ReadFile and treat os.IsNotExist as the default-config path.","Ensure no external process (backup/AV/sync) is replacing or locking the file during reads."],"exampleFix":"// before\nif !public.FileExists(filename) { /* default */ }\ndata, err := os.ReadFile(filename)\n// after\ndata, err := os.ReadFile(filename)\nif err != nil {\n    if os.IsNotExist(err) { /* provision default */ }\n    return FooterConfig{}, fmt.Errorf(\"error reading footer config file: %v\", err)\n}","handlingStrategy":"type-guard","validationCode":"fi, err := os.Stat(footerPath)\nif err != nil || !fi.Mode().IsRegular() {\n    return fmt.Errorf(\"footer_config.json is not a readable regular file\")\n}\nif f, err := os.Open(footerPath); err != nil { return err } else { f.Close() }","typeGuard":"func readableRegularFile(path string) bool {\n    fi, err := os.Stat(path)\n    if err != nil || !fi.Mode().IsRegular() { return false }\n    f, err := os.Open(path)\n    if err != nil { return false }\n    f.Close()\n    return true\n}","tryCatchPattern":"if _, err := askai.GetFooter(domain); err != nil {\n    if errors.Is(err, fs.ErrPermission) { /* chown/chmod file */ }\n    if errors.Is(err, fs.ErrNotExist) { /* raced delete: recreate or retry */ }\n    return err\n}","preventionTips":["Deploy config files with service-user ownership and 0644.","Never name directories or symlinks like config filenames.","Prefer single os.ReadFile + os.IsNotExist handling over FileExists+Read (avoids TOCTOU races)."],"tags":["go","filesystem","permissions","file-io","footer","askai"],"backgroundTag":"permission-denied","analyzedSha":"fc36c76c050c3775c5e899faf7403cf0262d2744","analyzedAt":"2026-09-05T21:28:54.019Z","contentChangedAt":"2026-09-05T21:28:54.019Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}