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 the

View on GitHub (pinned to 8cc6e09de2)

Solutions

  1. Stop routing traffic before/while closing sources: drain connections (readiness probe fail, SIGTERM handling) before calling Close
  2. Check server shutdown ordering so request handlers finish before sources are closed
  3. If triggered by LB draining, set a proper termination grace period and preStop hook
  4. 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

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


AI-assisted analysis of googleapis/mcp-toolbox@8cc6e09de2 (2026-09-05). Data as JSON: /api/errors/3bdcb64b5a27a02e. Report an issue: GitHub.