googleapis/mcp-toolbox · error
unable to initialize source %q: source is closed
Error message
unable to initialize source %q: source is closed
What it means
Returned by ConnectOnce.Do when the source has already been closed (c.closed set by Close) and a caller still tries to lazily initialize the connection. The library refuses to start a new connect attempt on a closed source, wrapping the error with the source name. This is a lifecycle misuse: use-after-close.
Source
Thrown at internal/sources/connect.go:166
return fmt.Errorf("unable to close source %q: %w", c.name, err)
}
return nil
}
// Do returns the connection, making it on the first call. Concurrent callers
// share one attempt, and a failed attempt is not remembered.
//
// Because a failure is retried by the next caller rather than cached, connect
// must release whatever it had already built before it returns an error. A
// pool that fails its ping and is returned unclosed leaks once per tool call,
// not once per process.
func (c *ConnectOnce[T]) Do(ctx context.Context, connect func(context.Context) (T, error)) (T, error) {
var zero T
if value, ok := c.Get(); ok {
return value, nil
}
if c.isClosed() {
return zero, fmt.Errorf("unable to initialize source %q: source is closed", c.name)
}
ch := c.initGroup.DoChan("", func() (any, error) {
// singleflight only shares an attempt that is still in flight, so a
// caller queued behind a finished winner would start a second connect.
if value, ok := c.Get(); ok {
return value, nil
}
if c.isClosed() {
return nil, fmt.Errorf("unable to initialize source %q: source is closed", c.name)
}
// The attempt is shared by every waiter and the handle outlives the
// request that triggered it, so it runs under the startup context
// rather than the caller's: a request context carries that caller's
// auth claims and a user agent that omits --user-agent-metadata, and it
// is cancelled when that one caller goes away. Only the span context
// crosses over, so the connect still appears in the trace of theView on GitHub (pinned to 8cc6e09de2)
Solutions
- Stop routing traffic before/while closing sources: drain connections (readiness probe fail, SIGTERM handling) before calling Close
- Check server shutdown ordering so request handlers finish before sources are closed
- If triggered by LB draining, set a proper termination grace period and preStop hook
- Audit application code for calls to the source after explicit Close
Defensive patterns
Strategy: retry
Try / catch
val, err := source.Do(ctx, connect)
if err != nil && strings.Contains(err.Error(), "source is closed") {
// fail fast; do not hammer — retry against another instance or after restart
return nil, status.Errorf(codes.Unavailable, "source is shutting down: %v", err)
} Prevention
- Drain traffic (readiness=fail, LB deregister) before closing sources
- Use graceful shutdown that waits for in-flight requests before Close
- Set adequate terminationGracePeriodSeconds in Kubernetes
- Avoid holding source references past explicit Close in app code
When it happens
Trigger: A request arrives after the server (or source manager) has begun shutting down and calls Do on a ConnectOnce whose Close already ran; Get() returns no value and isClosed() is true, so this error is returned immediately without any connect attempt.
Common situations: Requests in flight during graceful shutdown, load balancers still routing to a draining instance, or application code holding a source reference and invoking it after explicit Close.
Related errors
- unable to close source %q: %w
- error shutting down OpenTelemetry: %w
- error finding YAML files in %q: %w
- error finding YML files in %q: %w
- failed to initialize resources: %w
AI-assisted analysis of googleapis/mcp-toolbox@8cc6e09de2 (2026-09-05).
Data as JSON: /api/errors/3bdcb64b5a27a02e.
Report an issue: GitHub.