grpc/grpc-go · critical

The tap handle was already set and may not be reset.

Error message

The tap handle was already set and may not be reset.

What it means

grpc.InTapHandle (server.go:532) installs a single tap handle (tap.ServerInHandle) that can inspect/abort incoming RPCs at the transport level. Only one may be installed, so it panics if o.inTapHandle is already set. This is an EXPERIMENTAL API applied at NewServer time.

Source

Thrown at server.go:535

// while the last interceptor will be the inner most wrapper around the real call.
// All stream interceptors added by this method will be chained.
func ChainStreamInterceptor(interceptors ...StreamServerInterceptor) ServerOption {
	return newFuncServerOption(func(o *serverOptions) {
		o.chainStreamInts = append(o.chainStreamInts, interceptors...)
	})
}

// InTapHandle returns a ServerOption that sets the tap handle for all the server
// transport to be created. Only one can be installed.
//
// # Experimental
//
// Notice: This API is EXPERIMENTAL and may be changed or removed in a
// later release.
func InTapHandle(h tap.ServerInHandle) ServerOption {
	return newFuncServerOption(func(o *serverOptions) {
		if o.inTapHandle != nil {
			panic("The tap handle was already set and may not be reset.")
		}
		o.inTapHandle = h
	})
}

// StatsHandler returns a ServerOption that sets the stats handler for the server.
func StatsHandler(h stats.Handler) ServerOption {
	return newFuncServerOption(func(o *serverOptions) {
		if h == nil {
			logger.Error("ignoring nil parameter in grpc.StatsHandler ServerOption")
			// Do not allow a nil stats handler, which would otherwise cause
			// panics.
			return
		}
		o.statsHandlers = append(o.statsHandlers, h)
	})
}

View on GitHub (pinned to 03255a9237)

Solutions

  1. Ensure grpc.InTapHandle appears at most once across all options passed to NewServer.
  2. If you need multiple inspection points, compose them into a single tap handle that fans out internally.
  3. Remove the duplicate from whichever option slice is being merged.

Example fix

// before
srv := grpc.NewServer(
    grpc.InTapHandle(tapA),
    grpc.InTapHandle(tapB), // panics: already set
)

// after: compose into one handle
combined := &fanoutTap{handles: []tap.ServerInHandle{tapA, tapB}}
srv := grpc.NewServer(grpc.InTapHandle(combined))
Defensive patterns

Strategy: validation

Validate before calling

// Ensure at most one InTapHandle across option sources
var tapInstalled bool
for _, o := range opts {
    if isTapOption(o) { tapInstalled = true } // requires your own tagging
}
if !tapInstalled && myHandle != nil {
    opts = append(opts, grpc.InTapHandle(myHandle))
}

Prevention

When it happens

Trigger: Calling grpc.InTapHandle(h) twice in the options passed to NewServer, or combining an option slice that already carries an InTapHandle with another one.

Common situations: A shared server-setup helper installs a tap handle and application code adds its own; two libraries both register a tap handle; migrating configuration without removing the prior registration.

Related errors


AI-assisted analysis of grpc/grpc-go@03255a9237 (2026-08-07). Data as JSON: /api/errors/a66a63d0e47ef88c. Report an issue: GitHub.