{"record":{"id":"78c261191bcd8b42","repo":"router-for-me/CLIProxyAPI","slug":"failed-to-merge-metadata-w","errorCode":null,"errorMessage":"failed to merge metadata: %w","messagePattern":"failed to merge metadata: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/auth/claude/token.go","lineNumber":85,"sourceCode":"//\n// Parameters:\n//   - authFilePath: The full path where the token file should be saved\n//\n// Returns:\n//   - error: An error if the operation fails, nil otherwise\nfunc (ts *ClaudeTokenStorage) SaveTokenToFile(authFilePath string) error {\n\tmisc.LogSavingCredentials(authFilePath)\n\tts.Type = \"claude\"\n\n\t// Create directory structure if it doesn't exist\n\tif err := os.MkdirAll(filepath.Dir(authFilePath), 0700); err != nil {\n\t\treturn fmt.Errorf(\"failed to create directory: %v\", err)\n\t}\n\n\t// Merge metadata using helper\n\tdata, errMerge := misc.MergeMetadata(ts, ts.Metadata)\n\tif errMerge != nil {\n\t\treturn fmt.Errorf(\"failed to merge metadata: %w\", errMerge)\n\t}\n\n\t// Create the token file\n\tf, err := os.Create(authFilePath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create token file: %w\", err)\n\t}\n\tdefer func() {\n\t\tif errClose := f.Close(); errClose != nil {\n\t\t\tlog.Errorf(\"claude token storage: close token file error: %v\", errClose)\n\t\t}\n\t}()\n\n\t// Encode and write the token data as JSON\n\tif err = json.NewEncoder(f).Encode(data); err != nil {\n\t\treturn fmt.Errorf(\"failed to write token to file: %w\", err)\n\t}\n\treturn nil","sourceCodeStart":67,"sourceCodeEnd":103,"githubUrl":"https://github.com/router-for-me/CLIProxyAPI/blob/78f0c4079e3e6273d65d03b5549cffc898703264/internal/auth/claude/token.go#L67-L103","documentation":"Before writing the credential, SaveTokenToFile merges ts.Metadata into the storage struct via misc.MergeMetadata, which round-trips through JSON. This error means that merge failed — typically because the metadata (or an exported field on the token storage) contains a value JSON cannot marshal, such as a channel, func, or cyclic reference.","triggerScenarios":"A ClaudeTokenStorage whose Metadata map was populated with non-serializable values (func, chan, complex, or a cyclic struct) before SaveTokenToFile is called; metadata injected programmatically by an embedding SDK consumer rather than from parsed OAuth JSON.","commonSituations":"SDK/embedding users attaching runtime objects (loggers, callbacks) into Metadata instead of plain JSON values; custom builds that add struct fields of non-JSON types to ClaudeTokenStorage; virtually never happens in the stock CLI flow where metadata comes from JSON responses.","solutions":["Inspect ts.Metadata and ensure every value is JSON-serializable (strings, numbers, bools, plain maps/slices).","Keep runtime objects (loggers, hooks) in separate struct fields tagged json:\"-\" rather than in Metadata.","Log the wrapped marshal error to identify the offending key/type."],"exampleFix":"// before\nstorage.Metadata[\"cancel\"] = someFunc\n\n// after\nstorage.Metadata[\"note\"] = \"renewed 2026-08-15\" // metadata holds JSON values only","handlingStrategy":"validation","validationCode":"if _, err := json.Marshal(ts.Metadata); err != nil {\n    return fmt.Errorf(\"metadata must be JSON-serializable: %w\", err)\n}\nerr := ts.SaveTokenToFile(path)","typeGuard":"func jsonSafe(v any) bool { _, err := json.Marshal(v); return err == nil }","tryCatchPattern":"if err := ts.SaveTokenToFile(p); err != nil && strings.Contains(err.Error(), \"merge metadata\") {\n    ts.Metadata = map[string]any{} // strip non-serializable extras and retry\n    err = ts.SaveTokenToFile(p)\n}","preventionTips":["Keep Metadata limited to values that came from JSON.","Store runtime objects (loggers, hooks) in fields tagged json:\"-\"."],"tags":["claude","storage","json","metadata"],"backgroundTag":null,"analyzedSha":"78f0c4079e3e6273d65d03b5549cffc898703264","analyzedAt":"2026-08-15T12:26:37.444Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}