netbirdio/netbird · error
unexpected expose event: %T
Error message
unexpected expose event: %T
What it means
The first event received on the ExposeService stream was not the expected *proto.ExposeServiceEvent_Ready wrapper. The code uses a comma-ok type assertion on the event's oneof field and prints the actual Go type with %T. This guards the CLI/daemon event contract: the daemon is expected to send Ready first, and any other leading event means the two sides disagree on the protocol.
Source
Thrown at client/cmd/expose.go:245
return proto.ExposeProtocol_EXPOSE_TCP, nil
case expose.ProtocolUDP:
return proto.ExposeProtocol_EXPOSE_UDP, nil
case expose.ProtocolTLS:
return proto.ExposeProtocol_EXPOSE_TLS, nil
default:
return 0, fmt.Errorf("unhandled protocol type: %d", p)
}
}
func handleExposeReady(cmd *cobra.Command, stream proto.DaemonService_ExposeServiceClient, port uint64) error {
event, err := stream.Recv()
if err != nil {
return fmt.Errorf("receive expose event: %v", status.Convert(err).Message())
}
ready, ok := event.Event.(*proto.ExposeServiceEvent_Ready)
if !ok {
return fmt.Errorf("unexpected expose event: %T", event.Event)
}
printExposeReady(cmd, ready.Ready, port)
return nil
}
func printExposeReady(cmd *cobra.Command, r *proto.ExposeServiceReady, port uint64) {
cmd.Println("Service exposed successfully!")
cmd.Printf(" Name: %s\n", r.ServiceName)
if r.ServiceUrl != "" {
cmd.Printf(" URL: %s\n", r.ServiceUrl)
}
if r.Domain != "" && !isPortBasedProtocol(exposeProtocol) {
cmd.Printf(" Domain: %s\n", r.Domain)
}
cmd.Printf(" Protocol: %s\n", exposeProtocol)
cmd.Printf(" Internal: %d\n", port)
if isClusterProtocol(exposeProtocol) {
cmd.Printf(" External: %s\n", extractPort(r.ServiceUrl, resolveExternalPort(port)))View on GitHub (pinned to 93e97f4bf1)
Solutions
- Align the CLI and daemon on the same release: check `netbird version` and the daemon version in `netbird status`, and upgrade whichever is older
- Restart the daemon after upgrading so both sides speak the same event contract, then re-run
- If it persists on matched versions, report the event type shown in the message — it identifies which side introduced the divergence
Defensive patterns
Strategy: type-guard
Type guard
func isReadyEvent(e *proto.ExposeServiceEvent) (*proto.ExposeServiceReady, bool) {
if ready, ok := e.Event.(*proto.ExposeServiceEvent_Ready); ok {
return ready.Ready, true
}
return nil, false
} Prevention
- Keep the CLI and daemon on the same release so the event oneof contract matches
- When extending the event proto, keep Ready as the first event older CLIs expect, or version the negotiation
- Regenerate client/proto on both sides together after changing the .proto; never ship one half
When it happens
Trigger: A daemon from a different release emits a new event type (progress, log, or error event) before Ready; a fork extends proto.ExposeServiceEvent with extra oneof members and sends one first; a future protocol change reorders events.
Common situations: CLI and daemon binaries from different NetBird releases after a partial upgrade; custom builds where client/proto was regenerated on one side only.
Related errors
- failed to get log level: %v
- failed to set log level to TRACE: %v
- failed to get status: %v
- failed to set sync response persistence: %v
- connect to daemon: %w
AI-assisted analysis of netbirdio/netbird@93e97f4bf1 (2026-08-16).
Data as JSON: /api/errors/0e9f20e78286a51e.
Report an issue: GitHub.