{"record":{"id":"24ab2daab143c2e5","repo":"router-for-me/CLIProxyAPI","slug":"auth-file-name-must-end-with-json","errorCode":null,"errorMessage":"auth file name must end with .json","messagePattern":"auth file name must end with \\.json","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/pluginhost/auth_callbacks.go","lineNumber":268,"sourceCode":"\t\treturn nil, nil, fmt.Errorf(\"failed to read auth file: %w\", errRead)\n\t}\n\tif len(bytesTrimSpace(data)) == 0 {\n\t\treturn nil, nil, fmt.Errorf(\"auth file is empty for auth_index %s\", authIndex)\n\t}\n\tvar metadata map[string]any\n\tif errUnmarshal := json.Unmarshal(data, &metadata); errUnmarshal != nil {\n\t\treturn nil, nil, fmt.Errorf(\"invalid auth file for auth_index %s: %w\", authIndex, errUnmarshal)\n\t}\n\treturn auth, data, nil\n}\n\nfunc validateHostAuthSaveRequest(req pluginapi.HostAuthSaveRequest) (string, []byte, error) {\n\tname := strings.TrimSpace(req.Name)\n\tif isUnsafeAuthFileName(name) {\n\t\treturn \"\", nil, fmt.Errorf(\"invalid auth file name\")\n\t}\n\tif !strings.HasSuffix(strings.ToLower(name), \".json\") {\n\t\treturn \"\", nil, fmt.Errorf(\"auth file name must end with .json\")\n\t}\n\trawJSON := bytesTrimSpace(req.JSON)\n\tif len(rawJSON) == 0 {\n\t\treturn \"\", nil, fmt.Errorf(\"json is required\")\n\t}\n\tvar metadata map[string]any\n\tif errUnmarshal := json.Unmarshal(rawJSON, &metadata); errUnmarshal != nil {\n\t\treturn \"\", nil, fmt.Errorf(\"invalid auth json: %w\", errUnmarshal)\n\t}\n\treturn filepath.Base(name), rawJSON, nil\n}\n\nfunc (h *Host) saveAuthFile(ctx context.Context, name string, data []byte) (string, error) {\n\tauthDir := h.resolvedAuthDir()\n\tif authDir == \"\" {\n\t\treturn \"\", fmt.Errorf(\"auth directory is unavailable\")\n\t}\n\tdst := filepath.Join(authDir, filepath.Base(name))","sourceCodeStart":250,"sourceCodeEnd":286,"githubUrl":"https://github.com/router-for-me/CLIProxyAPI/blob/78f0c4079e3e6273d65d03b5549cffc898703264/internal/pluginhost/auth_callbacks.go#L250-L286","documentation":"Thrown by validateHostAuthSaveRequest when the (safe) file name does not end with .json (checked case-insensitively). Auth files must be JSON files so the loader and watchers can parse them; other extensions are rejected at save time.","triggerScenarios":"Plugin submits name 'credentials', 'auth.txt', or 'auth.JSON5' — anything whose lowercase form lacks the .json suffix.","commonSituations":"Plugin derives the extension from a provider config value; version bump of a plugin changed its naming convention; copy-paste of a non-Go filename convention.","solutions":["Append .json to the name before calling save if missing","Derive names from an existing listing (listAuthFiles returns *.json names) to match the convention"],"exampleFix":"// before\nname := \"openai-credentials\"\n\n// after\nif !strings.HasSuffix(strings.ToLower(name), \".json\") {\n    name += \".json\"\n}","handlingStrategy":"validation","validationCode":"if !strings.HasSuffix(strings.ToLower(name), \".json\") {\n    name += \".json\"\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Centralize auth file naming in one helper that enforces the .json suffix","Copy names from listAuthFiles output to stay consistent"],"tags":["go","pluginhost","auth","validation","file-extension"],"backgroundTag":null,"analyzedSha":"78f0c4079e3e6273d65d03b5549cffc898703264","analyzedAt":"2026-08-15T12:26:37.444Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}