{"record":{"id":"0e9f20e78286a51e","repo":"netbirdio/netbird","slug":"unexpected-expose-event-t","errorCode":null,"errorMessage":"unexpected expose event: %T","messagePattern":"unexpected expose event: %T","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"client/cmd/expose.go","lineNumber":245,"sourceCode":"\t\treturn proto.ExposeProtocol_EXPOSE_TCP, nil\n\tcase expose.ProtocolUDP:\n\t\treturn proto.ExposeProtocol_EXPOSE_UDP, nil\n\tcase expose.ProtocolTLS:\n\t\treturn proto.ExposeProtocol_EXPOSE_TLS, nil\n\tdefault:\n\t\treturn 0, fmt.Errorf(\"unhandled protocol type: %d\", p)\n\t}\n}\n\nfunc handleExposeReady(cmd *cobra.Command, stream proto.DaemonService_ExposeServiceClient, port uint64) error {\n\tevent, err := stream.Recv()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"receive expose event: %v\", status.Convert(err).Message())\n\t}\n\n\tready, ok := event.Event.(*proto.ExposeServiceEvent_Ready)\n\tif !ok {\n\t\treturn fmt.Errorf(\"unexpected expose event: %T\", event.Event)\n\t}\n\tprintExposeReady(cmd, ready.Ready, port)\n\treturn nil\n}\n\nfunc printExposeReady(cmd *cobra.Command, r *proto.ExposeServiceReady, port uint64) {\n\tcmd.Println(\"Service exposed successfully!\")\n\tcmd.Printf(\"  Name:     %s\\n\", r.ServiceName)\n\tif r.ServiceUrl != \"\" {\n\t\tcmd.Printf(\"  URL:      %s\\n\", r.ServiceUrl)\n\t}\n\tif r.Domain != \"\" && !isPortBasedProtocol(exposeProtocol) {\n\t\tcmd.Printf(\"  Domain:   %s\\n\", r.Domain)\n\t}\n\tcmd.Printf(\"  Protocol: %s\\n\", exposeProtocol)\n\tcmd.Printf(\"  Internal: %d\\n\", port)\n\tif isClusterProtocol(exposeProtocol) {\n\t\tcmd.Printf(\"  External: %s\\n\", extractPort(r.ServiceUrl, resolveExternalPort(port)))","sourceCodeStart":227,"sourceCodeEnd":263,"githubUrl":"https://github.com/netbirdio/netbird/blob/93e97f4bf1ad715072dcb3fb6cdb1763431b5a9c/client/cmd/expose.go#L227-L263","documentation":"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.","triggerScenarios":"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.","commonSituations":"CLI and daemon binaries from different NetBird releases after a partial upgrade; custom builds where client/proto was regenerated on one side only.","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"],"exampleFix":null,"handlingStrategy":"type-guard","validationCode":null,"typeGuard":"func isReadyEvent(e *proto.ExposeServiceEvent) (*proto.ExposeServiceReady, bool) {\n\tif ready, ok := e.Event.(*proto.ExposeServiceEvent_Ready); ok {\n\t\treturn ready.Ready, true\n\t}\n\treturn nil, false\n}","tryCatchPattern":null,"preventionTips":["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"],"tags":["grpc","type-assertion","oneof","version-mismatch","netbird-cli"],"backgroundTag":null,"analyzedSha":"93e97f4bf1ad715072dcb3fb6cdb1763431b5a9c","analyzedAt":"2026-08-16T03:09:19.136Z","schemaVersion":2},"datasetVersion":"2026-08-16T08:17:34.114Z"}