{"record":{"id":"370021ad77ce354c","repo":"grpc/grpc-go","slug":"xds-the-xds-client-is-closed","errorCode":null,"errorMessage":"xds: the xDS client is closed","messagePattern":"xds: the xDS client is closed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/xds/clients/xdsclient/xdsclient.go","lineNumber":226,"sourceCode":"\t<-c.serializer.Done()\n\n\tc.logger.Infof(\"Shutdown\")\n}\n\n// getChannelForADS returns an xdsChannel for the given server configuration.\n//\n// If an xdsChannel exists for the given server configuration, it is returned.\n// Else a new one is created. It also ensures that the calling authority is\n// added to the set of interested authorities for the returned channel.\n//\n// It returns the xdsChannel and a function to release the calling authority's\n// reference on the channel. The caller must call the cancel function when it is\n// no longer interested in this channel.\n//\n// A non-nil error is returned if an xdsChannel was not created.\nfunc (c *XDSClient) getChannelForADS(serverConfig *ServerConfig, callingAuthority *authority) (*xdsChannel, func(), error) {\n\tif c.done.HasFired() {\n\t\treturn nil, nil, errors.New(\"xds: the xDS client is closed\")\n\t}\n\n\tinitLocked := func(s *channelState) {\n\t\tif c.logger.V(2) {\n\t\t\tc.logger.Infof(\"Adding authority %q to the set of interested authorities for channel [%p]\", callingAuthority.name, s.channel)\n\t\t}\n\t\ts.interestedAuthorities[callingAuthority] = true\n\t}\n\tdeInitLocked := func(s *channelState) {\n\t\tif c.logger.V(2) {\n\t\t\tc.logger.Infof(\"Removing authority %q from the set of interested authorities for channel [%p]\", callingAuthority.name, s.channel)\n\t\t}\n\t\tdelete(s.interestedAuthorities, callingAuthority)\n\t}\n\n\treturn c.getOrCreateChannel(serverConfig, initLocked, deInitLocked)\n}\n","sourceCodeStart":208,"sourceCodeEnd":244,"githubUrl":"https://github.com/grpc/grpc-go/blob/0c51461d27177d997e14c642fe18c11668fc09a3/internal/xds/clients/xdsclient/xdsclient.go#L208-L244","documentation":"After `XDSClient.Close()` (xdsclient.go:177) fires the internal `done` event, any subsequent attempt to acquire a channel for ADS — via `getChannelForADS` (xdsclient.go:224) — returns this error at line 225-226. It signals that the client has been torn down and can no longer service watches or stream resources. The check is `c.done.HasFired()`.","triggerScenarios":"Triggered when a caller invokes WatchResource (or any other API that internally calls getChannelForADS) on an XDSClient whose Close() has already been called. This commonly occurs in shutdown races: one goroutine closes the client while another is still trying to register or re-register a watch.","commonSituations":"Concurrent shutdown where Close() races with watch registration; deferred Close() firing before a late-arriving watcher; a long-lived goroutine that outlives the client and attempts a new watch; a reconnection/retry loop that fires after the user closed the client.","solutions":["Coordinate shutdown: cancel all watchers (call the cancel funcs returned by WatchResource) before calling Close(), and ensure no goroutine can call WatchResource after Close.","Check whether the client is still alive (or wrap Close in a sync.Once gate) before issuing new watches from background goroutines.","Treat this error as terminal for the relevant goroutine — do not retry against the same closed client.","Use a context that is canceled together with Close so background loops exit before touching the client."],"exampleFix":"// before\ngo func() {\n   // ... periodic re-watch loop\n   client.WatchResource(typeURL, name, w) // races with Close()\n}()\nclient.Close()\n\n// after\ngo func() {\n   select {\n   case <-ctx.Done(): return // stop before Close()\n   default:\n   }\n   client.WatchResource(typeURL, name, w)\n}()\n// on shutdown:\ncancel()\nclient.Close()","handlingStrategy":"try-catch","validationCode":"// Track Close() so background goroutines can stop before touching the client.\ntype SafeXDSClient struct {\n    c       *xdsclient.XDSClient\n    closed  atomic.Bool\n    cancel  context.CancelFunc\n    ctx     context.Context\n}\n\nfunc (s *SafeXDSClient) Watch(typeURL, name string, w xdsclient.ResourceWatcher) (cancel func()) {\n    if s.closed.Load() {\n        return func() {}\n    }\n    return s.c.WatchResource(typeURL, name, w)\n}\n\nfunc (s *SafeXDSClient) Close() {\n    if s.closed.Swap(true) { return }\n    s.cancel()\n    s.c.Close()\n}","typeGuard":null,"tryCatchPattern":"// When a watch attempt might race with Close, treat this specific error as terminal.\ncancel := client.WatchResource(typeURL, name, w)\n// in error-handling paths that see \"xds: the xDS client is closed\":\nif errors.Is(err, errClientClosed) || strings.Contains(err.Error(), \"xDS client is closed\") {\n    return // stop the goroutine; do not retry against this client\n}","preventionTips":["Cancel all watchers and stop background goroutines before calling Close().","Wrap Close() in sync.Once and set an atomic flag background loops can read.","Bind a context.Context to the client's lifetime so loops exit when the context is canceled."],"tags":["grpc","xds","xdsclient","lifecycle","concurrency","use-after-close"],"backgroundTag":null,"analyzedSha":"0c51461d27177d997e14c642fe18c11668fc09a3","analyzedAt":"2026-08-11T14:49:15.055Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}