{"record":{"id":"52349aadee79fbf8","repo":"router-for-me/CLIProxyAPI","slug":"auth-provider-refresh-panic-v","errorCode":null,"errorMessage":"auth provider refresh panic: %v","messagePattern":"auth provider refresh panic: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/pluginhost/auth_provider.go","lineNumber":347,"sourceCode":"}\n\nfunc (h *Host) RefreshAuth(ctx context.Context, auth *coreauth.Auth) (refreshed *coreauth.Auth, handled bool, err error) {\n\tif h == nil || auth == nil {\n\t\treturn nil, false, nil\n\t}\n\trecord := h.authProviderRecord(authProvider(auth))\n\tif record == nil || record.plugin.Capabilities.AuthProvider == nil {\n\t\treturn nil, false, nil\n\t}\n\tif !h.recordCurrent(*record) {\n\t\treturn nil, false, nil\n\t}\n\tdefer func() {\n\t\tif recovered := recover(); recovered != nil {\n\t\t\th.fusePlugin(record.id, \"AuthProvider.RefreshAuth\", recovered)\n\t\t\trefreshed = nil\n\t\t\thandled = true\n\t\t\terr = fmt.Errorf(\"auth provider refresh panic: %v\", recovered)\n\t\t}\n\t}()\n\n\tpluginResp, errRefresh := record.plugin.Capabilities.AuthProvider.RefreshAuth(ctx, pluginapi.AuthRefreshRequest{\n\t\tAuthID:       authID(auth),\n\t\tAuthProvider: authProvider(auth),\n\t\tStorageJSON:  storageJSONFromAuth(auth),\n\t\tMetadata:     cloneAnyMap(authMetadata(auth)),\n\t\tAttributes:   authAttributes(auth),\n\t\tHost:         h.hostConfigSummary(),\n\t\tHTTPClient:   h.newHTTPClient(auth),\n\t})\n\tif errRefresh != nil {\n\t\treturn nil, true, errRefresh\n\t}\n\tdata := pluginResp.Auth\n\tif strings.TrimSpace(data.Provider) == \"\" {\n\t\tdata.Provider = authProvider(auth)","sourceCodeStart":329,"sourceCodeEnd":365,"githubUrl":"https://github.com/router-for-me/CLIProxyAPI/blob/78f0c4079e3e6273d65d03b5549cffc898703264/internal/pluginhost/auth_provider.go#L329-L365","documentation":"This error comes from the panic guard around a plugin's AuthProvider.RefreshAuth capability. When a plugin's token-refresh handler panics, the host recovers it, fuses the plugin, and returns this error with handled=true, meaning the refresh was claimed by the plugin but failed. The existing auth is left untouched and will be retried or expired according to normal policy.","triggerScenarios":"A background or on-demand token refresh for a provider served by a plugin whose RefreshAuth panics: invalid StorageJSON the plugin fails to parse defensively, unexpected nil Attributes/Metadata, or type assertion failures on cloned maps.","commonSituations":"A plugin whose stored credential format changed between versions (StorageJSON from an older auth file); auth files on disk written by a different plugin version; plugins that assume Metadata keys exist and dereference without checks.","solutions":["Read the fusePlugin log for 'AuthProvider.RefreshAuth' plus the recovered value to identify the plugin and panic cause.","Check the stored auth file (auth dir) for the affected provider: malformed or version-skewed StorageJSON is the most common trigger.","Re-authenticate the provider (delete the stale auth entry and redo login) so the plugin receives fresh, well-formed StorageJSON.","Fix the plugin to validate StorageJSON (json.Unmarshal errors returned, not panicked) and nil-check Metadata/Attributes.","Update the plugin to a version compatible with the current pluginapi.AuthRefreshRequest shape."],"exampleFix":"// plugin side, before\nfunc (p *Provider) RefreshAuth(ctx context.Context, req pluginapi.AuthRefreshRequest) (pluginapi.AuthRefreshResponse, error) {\n\tvar st stored\n\tjson.Unmarshal([]byte(req.StorageJSON), &st) // error ignored\n\treturn pluginapi.AuthRefreshResponse{AccessToken: st.Token.Value}, nil // panics if unmarshal failed\n}\n\n// after\nfunc (p *Provider) RefreshAuth(ctx context.Context, req pluginapi.AuthRefreshRequest) (pluginapi.AuthRefreshResponse, error) {\n\tvar st stored\n\tif err := json.Unmarshal([]byte(req.StorageJSON), &st); err != nil {\n\t\treturn pluginapi.AuthRefreshResponse{}, fmt.Errorf(\"refresh: decode storage: %w\", err)\n\t}\n\tif st.Token.Value == \"\" {\n\t\treturn pluginapi.AuthRefreshResponse{}, fmt.Errorf(\"refresh: stored token is empty\")\n\t}\n\treturn pluginapi.AuthRefreshResponse{AccessToken: st.Token.Value}, nil\n}","handlingStrategy":"try-catch","validationCode":"// Skip plugin refresh for providers whose auth storage is empty/malformed\nif strings.TrimSpace(auth.StorageJSON()) == \"\" {\n    return nil, fmt.Errorf(\"auth %s has no storage blob; re-login required\", auth.ID)\n}","typeGuard":"func refreshSafe(h *pluginhost.Host, auth coreauth.Auth) bool {\n    rec := h.AuthProviderRecord(auth.Provider())\n    return rec != nil && !h.IsPluginFused(rec.ID) && strings.TrimSpace(auth.StorageJSON()) != \"\"\n}","tryCatchPattern":"refreshed, handled, err := host.CallRefreshAuth(ctx, auth)\nif err != nil {\n    if strings.Contains(err.Error(), \"refresh panic\") {\n        // handled=true: plugin claimed refresh but crashed; keep old auth and schedule re-login\n        log.WithError(err).Warn(\"plugin refresh panicked; retaining previous auth\")\n        return auth, nil\n    }\n    return nil, err\n}","preventionTips":["Validate plugin-written StorageJSON before relying on it for refresh.","Re-login after plugin upgrades that change storage format.","Alert on repeated fuse events for auth plugins so refreshes fail over to re-login early."],"tags":["plugin","panic","auth","token-refresh","pluginhost"],"backgroundTag":null,"analyzedSha":"78f0c4079e3e6273d65d03b5549cffc898703264","analyzedAt":"2026-08-15T12:26:37.444Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}