{"id":"d2bc2e45958414f7","repo":"go-redis/redis","slug":"failed-to-subscribe-to-streaming-credentials-w","errorCode":null,"errorMessage":"failed to subscribe to streaming credentials: %w","messagePattern":"failed to subscribe to streaming credentials: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"redis.go","lineNumber":753,"sourceCode":"\tconn.baseClient.allowClientTracking = true\n\n\tusername, password := \"\", \"\"\n\tif c.opt.StreamingCredentialsProvider != nil {\n\t\tcredListener, initErr := c.streamingCredentialsManager.Listener(\n\t\t\tcn,\n\t\t\tc.reAuthConnection(),\n\t\t\tc.onAuthenticationErr(),\n\t\t)\n\t\tif initErr != nil {\n\t\t\tcn.GetStateMachine().Transition(pool.StateClosed)\n\t\t\treturn fmt.Errorf(\"failed to create credentials listener: %w\", initErr)\n\t\t}\n\n\t\tcredentials, unsubscribeFromCredentialsProvider, initErr := c.opt.StreamingCredentialsProvider.\n\t\t\tSubscribe(credListener)\n\t\tif initErr != nil {\n\t\t\tcn.GetStateMachine().Transition(pool.StateClosed)\n\t\t\treturn fmt.Errorf(\"failed to subscribe to streaming credentials: %w\", initErr)\n\t\t}\n\n\t\t// Per-connection unsubscribe is attached to the connection itself so it\n\t\t// runs when this specific connection is closed. Do not register it on\n\t\t// c.onClose: initConn runs for every (re)initialized connection, and\n\t\t// attaching per-connection state to the shared baseClient registry would\n\t\t// either leak entries (one per connection id, never trimmed) or — with\n\t\t// the pre-fix wrappedOnClose approach — build an unbounded closure chain\n\t\t// retaining every prior connection's unsubscribe (see issue #3772).\n\t\t//\n\t\t// Note: pool.Conn.SetOnClose OVERWRITES any prior callback (see the\n\t\t// doc on that method). That is safe here because the streaming\n\t\t// credentials Manager deduplicates listeners by connection id, so a\n\t\t// second initConn on the same cn re-Subscribes the SAME listener and\n\t\t// the returned unsubscribe is equivalent to the one already installed.\n\t\t// Any future code path that could hand out a distinct unsubscribe on\n\t\t// re-initialization must first invoke the existing one to avoid\n\t\t// orphaning the old subscription on the credentials provider.","sourceCodeStart":735,"sourceCodeEnd":771,"githubUrl":"https://github.com/go-redis/redis/blob/36d97525cd8076aed67cddf54778e9ea84550929/redis.go#L735-L771","documentation":"Thrown during per-connection initialization when Options.StreamingCredentialsProvider.Subscribe(listener) returns an error. Every new/reinitialized pool connection subscribes a listener so the client can re-AUTH on token rotation; if the provider refuses the subscription (e.g. it is closed or the identity backend is unreachable), the connection is transitioned to StateClosed and the error is wrapped and surfaced through the dialer.","triggerScenarios":"Setting Options.StreamingCredentialsProvider (e.g. the Entra ID provider from go-redis-entraid) and then performing any operation that dials a connection, while the provider's Subscribe() returns a non-nil error. Also triggered on reconnect/reinit since initConn runs for every connection.","commonSituations":"Streaming credentials provider was Close()d before the client issued commands; misconfigured tenant/client ID/secret against the identity provider; the IdP token endpoint is unreachable (DNS, firewall, proxy); provider initialized with an expired/invalid cache; network partition between the app and the OAuth issuer.","solutions":["Verify the streaming credentials provider is still open and was constructed with valid IdP settings (tenant, client ID, secret/scope) before the client is used.","Check network connectivity from the app host to the identity provider token endpoint (TLS handshake, proxy env vars, DNS).","If streaming credentials are optional, remove StreamingCredentialsProvider and fall back to static Username/Password or a context/function provider until the IdP issue is resolved.","Inspect the wrapped error (%w) for the underlying cause; provider implementations usually expose a typed error you can branch on."],"exampleFix":"// before\nprovider := entraid.NewProvider(entraid.Config{TenantID: tenant, ClientID: cid})\nopt.StreamingCredentialsProvider = provider\nclient := redis.NewClient(opt)\n// provider.Subscribe fails because the secret was never set\n\n// after\nprovider, err := entraid.NewProvider(entraid.Config{\n    TenantID: tenant, ClientID: cid, ClientSecret: secret,\n})\nif err != nil { return err }\nopt.StreamingCredentialsProvider = provider\nclient := redis.NewClient(opt)","handlingStrategy":"validation","validationCode":"// Validate the streaming provider can subscribe before constructing the client.\n// Most providers expose a Health/Ping or a dry-run Subscribe you can probe.\nif p, ok := opt.StreamingCredentialsProvider.(interface{ Healthy() bool }); ok && !p.Healthy() {\n    return errors.New(\"streaming credentials provider is not healthy\")\n}","typeGuard":null,"tryCatchPattern":"// Wrap the first command so dial/init errors surface as normal Go errors.\nif err := client.Ping(ctx).Err(); err != nil {\n    var se *streaming.SubscribeError // provider-specific typed error if exposed\n    if errors.As(err, &se) {\n        // handle provider-side failure (recreate provider, fall back)\n    }\n    return err\n}","preventionTips":["Construct the streaming credentials provider once and reuse it; do not Close() it while the client is alive.","Add a startup readiness probe that calls Ping before marking the service healthy.","Provide a fallback CredentialsProviderFunc so a streaming-provider outage degrades instead of failing."],"tags":["authentication","streaming-credentials","config","network"],"analyzedSha":"36d97525cd8076aed67cddf54778e9ea84550929","analyzedAt":"2026-08-06T01:08:27.376Z","schemaVersion":2}