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.

Solutions

  1. Add the missing case to the type switch in ConfigEntryFromStructs (and the enum case in ConfigEntryToStructs)
  2. Unwrap decorated/wrapped entries to their underlying concrete type before calling the converter
  3. Nil-check the entry before conversion
  4. 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

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


AI-assisted analysis of hashicorp/consul@2397ff0d76 (2026-08-15). Data as JSON: /api/errors/e50858ed21506302. Report an issue: GitHub.

Appendix: 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)