hashicorp/consul · error
unable to convert %T to proto
Error message
unable to convert %T to proto
What it means
ConfigEntryFromStructs (proto/private/pbconfigentry/config_entry.go:264 region) panics when the concrete Go type of the structs.ConfigEntry interface value is not one of the handled cases (e.g. *structs.MeshConfigEntry, *structs.ServiceResolverConfigEntry, ... *structs.ExportedServicesConfigEntry). Unlike a missing enum case, this is a Go type-switch exhaustiveness problem: any wrapper/decorator implementing the interface, any newly added entry type, and a nil interface all fall through. The %T in the message tells you exactly which concrete type was rejected.
Source
Thrown at proto/private/pbconfigentry/config_entry.go:264
}
case *structs.JWTProviderConfigEntry:
var jwtProvider JWTProvider
JWTProviderFromStructs(v, &jwtProvider)
configEntry.Kind = Kind_KindJWTProvider
configEntry.Entry = &ConfigEntry_JWTProvider{
JWTProvider: &jwtProvider,
}
case *structs.ExportedServicesConfigEntry:
var es ExportedServices
ExportedServicesFromStructs(v, &es)
configEntry.Kind = Kind_KindExportedServices
configEntry.Entry = &ConfigEntry_ExportedServices{
ExportedServices: &es,
}
default:
panic(fmt.Sprintf("unable to convert %T to proto", s))
}
return configEntry
}
func tlsVersionToStructs(s string) types.TLSVersion {
return types.TLSVersion(s)
}
func tlsVersionFromStructs(t types.TLSVersion) string {
return t.String()
}
func cipherSuitesToStructs(cs []string) []types.TLSCipherSuite {
cipherSuites := make([]types.TLSCipherSuite, len(cs))
for idx, suite := range cs {
cipherSuites[idx] = types.TLSCipherSuite(suite)
}View on GitHub (pinned to 2397ff0d76)
Solutions
- Add the missing case to the type switch in ConfigEntryFromStructs (and the enum case in ConfigEntryToStructs)
- Unwrap decorated/wrapped entries to their underlying concrete type before calling the converter
- Nil-check the entry before conversion
- Keep mixed-version clusters aligned so all binaries know the same entry types
Example fix
// before
pbEntry := pbconfigentry.ConfigEntryFromStructs(wrappedEntry) // wrapper type -> panic
// after
switch e := wrappedEntry.(type) {
case *structs.ServiceDefaultsConfigEntry, *structs.MeshConfigEntry:
pbEntry = pbconfigentry.ConfigEntryFromStructs(e)
default:
return fmt.Errorf("unsupported config entry type %T", e)
} Defensive patterns
Strategy: type-guard
Validate before calling
func registeredConfigEntry(s structs.ConfigEntry) bool {
switch s.(type) {
case *structs.MeshConfigEntry, *structs.ServiceResolverConfigEntry, *structs.IngressGatewayConfigEntry,
*structs.ServiceIntentionsConfigEntry, *structs.ServiceConfigEntry, *structs.APIGatewayConfigEntry,
*structs.BoundAPIGatewayConfigEntry, *structs.TCPRouteConfigEntry, *structs.HTTPRouteConfigEntry,
*structs.FileSystemCertificateConfigEntry, *structs.InlineCertificateConfigEntry,
*structs.SamenessGroupConfigEntry, *structs.JWTProviderConfigEntry, *structs.ExportedServicesConfigEntry:
return true
}
return false
}
if s == nil || !registeredConfigEntry(s) {
return fmt.Errorf("unsupported config entry type %T", s)
}
pbEntry := pbconfigentry.ConfigEntryFromStructs(s) Type guard
func asRegisteredEntry(s structs.ConfigEntry) (structs.ConfigEntry, bool) {
if s == nil {
return nil, false
}
if w, ok := s.(interface{ Unwrap() structs.ConfigEntry }); ok {
s = w.Unwrap() // peel wrappers/decorators
}
return s, registeredConfigEntry(s)
} Try / catch
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("config entry serialization failed for %T: %v", entry, r)
}
}() Prevention
- Never pass wrapper/mock implementations of structs.ConfigEntry into proto converters; unwrap first
- When adding a new entry kind, update both switch statements in proto/private/pbconfigentry/config_entry.go together
- Nil-check the interface before conversion; nil also falls through to the panic
- Let the %T in the panic message drive the fix: it names the exact offending type
When it happens
Trigger: Passing a new structs.*ConfigEntry kind that has no case in the switch; passing a wrapper or test double that implements structs.ConfigEntry around a real entry; passing a nil structs.ConfigEntry; enterprise/CE divergence where one side has types the other lacks.
Common situations: Consul development adding a new config entry kind without updating both converters; wrapping entries for validation/mutation and forgetting to unwrap before serialization; test helpers that build mock ConfigEntry implementations.
Related errors
- unable to convert ConfigEntry of kind %s to structs
- client streams are unsupported
- invalid type: %T
- ClusterSize not set
- failed to decode request: %v
AI-assisted analysis of hashicorp/consul@2397ff0d76 (2026-08-15).
Data as JSON: /api/errors/e50858ed21506302.
Report an issue: GitHub.