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
- Ensure grpc.InTapHandle appears at most once across all options passed to NewServer.
- If you need multiple inspection points, compose them into a single tap handle that fans out internally.
- 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
- Treat InTapHandle as a singleton; if two components need tap inspection, compose them into one handle.
- Confirm no shared library registers an InTapHandle before adding your own.
- Remember InTapHandle is EXPERIMENTAL; prefer StatsHandler where possible.
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
- The unary server interceptor was already set and may not be
- The stream server interceptor was already set and may not be
- cannot use component logger as grpclog logger
- grpc: no transport security set (use grpc.WithTransportCrede
- grpc: credentials.Bundle may not be used with individual Tra
AI-assisted analysis of grpc/grpc-go@03255a9237 (2026-08-07).
Data as JSON: /api/errors/a66a63d0e47ef88c.
Report an issue: GitHub.