grpc/grpc-go · critical
The unary server interceptor was already set and may not be
Error message
The unary server interceptor was already set and may not be reset.
What it means
grpc.UnaryInterceptor (server.go:485) installs a single non-chained unary server interceptor. gRPC forbids setting it twice because a second call would silently overwrite the first (a security/observability footgun), so it panics if o.unaryInt is already non-nil. This option is applied during server construction, so the panic happens at NewServer time.
Source
Thrown at server.go:488
return newFuncServerOption(func(o *serverOptions) {
o.maxConcurrentStreams = n
})
}
// Creds returns a ServerOption that sets credentials for server connections.
func Creds(c credentials.TransportCredentials) ServerOption {
return newFuncServerOption(func(o *serverOptions) {
o.creds = c
})
}
// UnaryInterceptor returns a ServerOption that sets the UnaryServerInterceptor for the
// server. Only one unary interceptor can be installed. The construction of multiple
// interceptors (e.g., chaining) can be implemented at the caller.
func UnaryInterceptor(i UnaryServerInterceptor) ServerOption {
return newFuncServerOption(func(o *serverOptions) {
if o.unaryInt != nil {
panic("The unary server interceptor was already set and may not be reset.")
}
o.unaryInt = i
})
}
// ChainUnaryInterceptor returns a ServerOption that specifies the chained interceptor
// for unary RPCs. The first interceptor will be the outer most,
// while the last interceptor will be the inner most wrapper around the real call.
// All unary interceptors added by this method will be chained.
func ChainUnaryInterceptor(interceptors ...UnaryServerInterceptor) ServerOption {
return newFuncServerOption(func(o *serverOptions) {
o.chainUnaryInts = append(o.chainUnaryInts, interceptors...)
})
}
// StreamInterceptor returns a ServerOption that sets the StreamServerInterceptor for the
// server. Only one stream interceptor can be installed.
func StreamInterceptor(i StreamServerInterceptor) ServerOption {View on GitHub (pinned to 03255a9237)
Solutions
- Replace multiple grpc.UnaryInterceptor calls with a single grpc.ChainUnaryInterceptor(a, b, ...) which supports many interceptors.
- Search the combined option slice passed to NewServer for duplicate UnaryInterceptor entries and keep only one.
- If a framework injects one, disable its interceptor or use its chaining hook instead of adding your own UnaryInterceptor.
Example fix
// before
srv := grpc.NewServer(
grpc.UnaryInterceptor(loggingInterceptor),
grpc.UnaryInterceptor(authInterceptor), // panics: already set
)
// after
srv := grpc.NewServer(
grpc.ChainUnaryInterceptor(loggingInterceptor, authInterceptor),
) Defensive patterns
Strategy: validation
Validate before calling
// Detect duplicate UnaryInterceptor in assembled options before NewServer
func hasUnaryInterceptor(opts []grpc.ServerOption) bool {
// grpc.ServerOption is opaque; instead, manage interceptors through a
// single ChainUnaryInterceptor so duplicates are structurally impossible.
return false
}
// Prefer: collect interceptors in a slice, register once via ChainUnaryInterceptor. Prevention
- Standardize on ChainUnaryInterceptor everywhere; ban raw UnaryInterceptor in shared builders.
- Keep a single place that assembles server options to avoid merging two slices that each carry an interceptor.
- Document that single-interceptor options are one-shot.
When it happens
Trigger: Passing grpc.UnaryInterceptor(...) twice in the same NewServer(opts...) call, or a helper that bundles a UnaryInterceptor option being combined with an explicit UnaryInterceptor at the call site, so that o.unaryInt is set on the second invocation.
Common situations: A shared option list already contains a unary interceptor (e.g. from a framework) and application code adds another; migrating from single to chained interceptors but forgetting to remove the old UnaryInterceptor call; merging two option slices that each carry one.
Related errors
- The stream server interceptor was already set and may not be
- The tap handle was already set and may not be reset.
- 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/bc8df2e38a2ff546.
Report an issue: GitHub.