{"record":{"id":"cb9a1c3047d5fe53","repo":"charmbracelet/crush","slug":"provider-s-not-found","errorCode":null,"errorMessage":"provider %s not found","messagePattern":"provider (.+?) not found","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/config/store.go","lineNumber":673,"sourceCode":"//     read-decide-exchange-write cycle, so only one process exchanges at a\n//     time. A process that acquires the lock after a peer rotated finds the\n//     peer's fresh token on disk and adopts it instead of exchanging.\nfunc (s *ConfigStore) RefreshOAuthToken(ctx context.Context, scope Scope, providerID string) error {\n\tkey := fmt.Sprintf(\"%d\\x00%s\", scope, providerID)\n\t_, err, _ := s.refreshSF.Do(key, func() (any, error) {\n\t\treturn nil, s.refreshOAuthTokenLocked(ctx, scope, providerID)\n\t})\n\treturn err\n}\n\n// refreshOAuthTokenLocked performs the cross-process single-flighted\n// refresh. It is invoked through refreshSF, so at most one goroutine per\n// provider runs it at a time within this process.\nfunc (s *ConfigStore) refreshOAuthTokenLocked(ctx context.Context, scope Scope, providerID string) error {\n\tcfg := s.Config()\n\tproviderConfig, exists := cfg.Providers.Get(providerID)\n\tif !exists {\n\t\treturn fmt.Errorf(\"provider %s not found\", providerID)\n\t}\n\tif providerConfig.OAuthToken == nil {\n\t\treturn fmt.Errorf(\"provider %s does not have an OAuth token\", providerID)\n\t}\n\tentryToken := providerConfig.OAuthToken\n\n\t// Acquire the per-provider cross-process refresh lock. This is a\n\t// dedicated lock file, not the config-write lock, and it does not take\n\t// s.mu — so the network exchange below cannot stall unrelated config\n\t// operations. The deadline exceeds the exchange timeout so that a peer\n\t// mid-exchange has time to publish a token we can adopt. Lock ordering:\n\t// the refresh lock is always taken before the config-write lock (via\n\t// SetConfigFields), never the reverse, so no deadlock is possible.\n\tlockCtx, cancel := context.WithTimeout(ctx, refreshLockDeadline)\n\tdefer cancel()\n\trelease, lockErr := lock.File(lockCtx, s.refreshLockPath(providerID))\n\tif lockErr != nil {\n\t\t// Could not acquire the lock (peer wedged or deadline hit). Prefer a","sourceCodeStart":655,"sourceCodeEnd":691,"githubUrl":"https://github.com/charmbracelet/crush/blob/7944b8e52225d8805e31eacbf7ef24856b0dfb7a/internal/config/store.go#L655-L691","documentation":"refreshOAuthTokenLocked refreshes an expired OAuth token for a provider. Before doing any network work it looks the provider up in the live in-memory config; if no provider with that ID is configured it returns this error, since there is nothing to refresh.","triggerScenarios":"The background refresh path (invoked via refreshSF with single-flight per provider) fires for a providerID that was removed from the config (user deleted it, config file reloaded without it) or never existed / is misspelled.","commonSituations":"Provider removed from crush.json while a token refresh was pending; config reloaded from disk dropping the provider between auth and refresh; referencing a provider by an ID that differs in case or spelling from the configured one.","solutions":["Re-add the provider to the config (crush login <provider>) to restore its OAuth token","Confirm the provider ID used matches the one in the config file exactly","Reload/restart so in-memory config matches the file, then retry the request","If the provider was intentionally removed, clear any cached clients referencing it"],"exampleFix":"// before\nresp, err := client.Do(ctx, req) // internally triggers refresh for \"copilot\"\n// after\nif _, ok := store.Config().Providers.Get(providerID); !ok {\n    return nil, fmt.Errorf(\"provider %q not configured; run 'crush login %s' first\", providerID, providerID)\n}\nresp, err := client.Do(ctx, req)","handlingStrategy":"validation","validationCode":"if _, ok := store.Config().Providers.Get(providerID); !ok {\n    return fmt.Errorf(\"provider %q is not configured; skipping token refresh\", providerID)\n}","typeGuard":"func providerConfigured(store *config.ConfigStore, id string) bool {\n    _, ok := store.Config().Providers.Get(id)\n    return ok\n}","tryCatchPattern":"if err := doRequest(ctx); err != nil {\n    if strings.Contains(err.Error(), \"provider \") && strings.Contains(err.Error(), \"not found\") {\n        return fmt.Errorf(\"provider removed from config; re-run 'crush login' to re-authenticate: %w\", err)\n    }\n    return err\n}","preventionTips":["Cancel pending token refresh timers when removing a provider from config","Match provider IDs exactly (case/spelling) between config and API calls","Reload the store after config file edits so in-memory state matches disk","Re-authenticate instead of retrying when the provider was intentionally deleted"],"tags":["oauth","provider-id","token-refresh"],"backgroundTag":"oauth-provider-not-configured","analyzedSha":"7944b8e52225d8805e31eacbf7ef24856b0dfb7a","analyzedAt":"2026-08-29T12:48:59.079Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}