{"record":{"id":"9f18ae3440e33ee6","repo":"Billionmail/BillionMail","slug":"error-reading-prompt-config-file-v","errorCode":null,"errorMessage":"error reading prompt config file: %v","messagePattern":"error reading prompt config file: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"core/internal/service/askai/project.go","lineNumber":801,"sourceCode":"\t\t// not found errors.\n\t\tdefaultPromptConfig := PromptConfig{\n\t\t\tPrompt: \"This is a default prompt. Please customize it.\",\n\t\t}\n\t\tdefaultPromptConfig.UpdateTime = public.GetNowTime()\n\t\tdefaultPromptJson, err := json.MarshalIndent(defaultPromptConfig, \"\", \"  \")\n\n\t\tif err != nil {\n\t\t\treturn PromptConfig{}, fmt.Errorf(\"error marshalling default prompt config: %v\", err)\n\t\t}\n\t\terr = os.WriteFile(filename, defaultPromptJson, 0644)\n\t\tif err != nil {\n\t\t\treturn PromptConfig{}, fmt.Errorf(\"error saving default prompt config file: %v\", err)\n\t\t}\n\t\treturn defaultPromptConfig, nil\n\t}\n\tdata, err := os.ReadFile(filename)\n\tif err != nil {\n\t\treturn PromptConfig{}, fmt.Errorf(\"error reading prompt config file: %v\", err)\n\t}\n\n\tvar config PromptConfig\n\terr = json.Unmarshal(data, &config)\n\tif err != nil {\n\t\treturn PromptConfig{}, fmt.Errorf(\"error unmarshalling prompt config: %v\", err)\n\t}\n\treturn config, nil\n}\n\n// ModifyPrompt updates the prompt configuration for a given domain.\n// It reads the existing prompt configuration, modifies the specified fields, and saves the updated configuration back to the file.\n// If the prompt is empty, it will not be updated.\nfunc ModifyPrompt(Domain string, Prompt string) error {\n\tconfig, err := GetPrompt(Domain)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error getting prompt config: %v\", err)\n\t}","sourceCodeStart":783,"sourceCodeEnd":819,"githubUrl":"https://github.com/Billionmail/BillionMail/blob/fc36c76c050c3775c5e899faf7403cf0262d2744/core/internal/service/askai/project.go#L783-L819","documentation":"GetPrompt fails at project.go:801 when os.ReadFile cannot read the existing PRODUCT_CONFIG_PATH/<domain>/prompt_config.json. Since the code already checks public.FileExists, ENOENT here usually means a race (file deleted between the check and the read) or a broken symlink; other causes are permission denial on the file or directory, or an I/O error. The wrapped *fs.PathError names the path and errno.","triggerScenarios":"Calling GetPrompt(domain) (or ModifyPrompt) when prompt_config.json exists in the FileExists check but becomes unreadable: deleted concurrently by another process, permission bits (0644 in a dir the service user cannot traverse), symlink pointing nowhere, or EIO on storage.","commonSituations":"Concurrent deployments or cleanup jobs removing config files while the app reads them; config dir copied/rsynced with wrong ownership; Docker volume permission mismatch after image user change; corrupted/failed mount.","solutions":["Inspect the wrapped *fs.PathError errno: ENOENT → race/dead symlink, EACCES → permissions, EIO → storage.","Check file and directory permissions so the service user can read prompt_config.json and traverse the domain dir.","Replace the FileExists check + read with a single read and treat os.IsNotExist as the default-config case, eliminating the race.","Ensure no cron/deployment job deletes config files while the service runs."],"exampleFix":"// before\nif !public.FileExists(filename) { ...default config... }\ndata, err := os.ReadFile(filename)\nif err != nil {\n    return PromptConfig{}, fmt.Errorf(\"error reading prompt config file: %v\", err)\n}\n// after\ndata, err := os.ReadFile(filename)\nif err != nil {\n    if os.IsNotExist(err) {\n        ...create and return default config...\n    }\n    return PromptConfig{}, fmt.Errorf(\"error reading prompt config file: %v\", err)\n}","handlingStrategy":"fallback","validationCode":"filename := fmt.Sprintf(PRODUCT_CONFIG_PATH+\"/%s/prompt_config.json\", domain)\nif info, err := os.Stat(filename); err != nil || info.IsDir() {\n    // file missing or not a regular file — prepare to use/create default config\n}\nif f, err := os.Open(filename); err != nil {\n    return fmt.Errorf(\"file unreadable: %v\", err)\n} else {\n    f.Close()\n}","typeGuard":"func promptConfigReadable(path string) bool {\n    f, err := os.Open(path)\n    if err != nil {\n        return false\n    }\n    f.Close()\n    return true\n}","tryCatchPattern":"cfg, err := askai.GetPrompt(domain)\nif err != nil {\n    var perr *fs.PathError\n    if errors.As(err, &perr) && errors.Is(perr.Err, fs.ErrNotExist) {\n        // fall back to defaults instead of failing\n        cfg = askai.DefaultPromptConfig()\n    } else if errors.As(err, &perr) && errors.Is(perr.Err, fs.ErrPermission) {\n        log.Printf(\"cannot read %s — fix ownership/permissions\", perr.Path)\n        return err\n    } else {\n        return err\n    }\n}","preventionTips":["Replace exists-check + read with a single os.ReadFile and handle os.IsNotExist as the default-config path, removing the race window.","Fix file permissions/ownership after copying or syncing config directories.","Prevent cleanup or deploy jobs from deleting config files while the service runs.","Fall back to defaults on ENOENT only; surface EACCES/EIO as real errors."],"tags":["go","filesystem","permissions","race-condition","config"],"backgroundTag":"file-read-failed","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"}