{"record":{"id":"38d4d15a32f4d2bd","repo":"netbirdio/netbird","slug":"expose-manager-not-available","errorCode":null,"errorMessage":"expose manager not available","messagePattern":"expose manager not available","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"client/embed/embed.go","lineNumber":433,"sourceCode":"\t\tDialContext: c.Dial,\n\t}\n\n\treturn &http.Client{\n\t\tTransport: transport,\n\t}\n}\n\n// Expose exposes a local service via the NetBird reverse proxy, making it accessible through a public URL.\n// It returns an ExposeSession. Call Wait on the session to keep it alive.\nfunc (c *Client) Expose(ctx context.Context, req ExposeRequest) (*ExposeSession, error) {\n\tengine, err := c.getEngine()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tmgr := engine.GetExposeManager()\n\tif mgr == nil {\n\t\treturn nil, fmt.Errorf(\"expose manager not available\")\n\t}\n\n\tresp, err := mgr.Expose(ctx, req)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"expose: %w\", err)\n\t}\n\n\treturn &ExposeSession{\n\t\tDomain:      resp.Domain,\n\t\tServiceName: resp.ServiceName,\n\t\tServiceURL:  resp.ServiceURL,\n\t\tmgr:         mgr,\n\t}, nil\n}\n\n// IdentityForIP looks up a remote peer by its tunnel IP using the\n// embedded client's status recorder. Returns the peer's WireGuard public\n// key and FQDN. ok=false means the IP doesn't belong to an active peer","sourceCodeStart":415,"sourceCodeEnd":451,"githubUrl":"https://github.com/netbirdio/netbird/blob/93e97f4bf1ad715072dcb3fb6cdb1763431b5a9c/client/embed/embed.go#L415-L451","documentation":"Returned by Client.Expose when engine.GetExposeManager() returns nil. The expose manager is only created during Engine.Start (client/internal/engine.go:545), so a nil manager means the engine has not completed its start phase under this engine instance. getEngine already rejected not-started clients, so in practice this surfaces when the engine exists but its start path that installs the expose manager has not run.","triggerScenarios":"Calling Expose on a Client whose engine object exists but is still initializing, or on an engine whose start failed/was aborted before the expose manager was created.","commonSituations":"Racing Expose against a Start call that has not returned yet; calling Expose on a second Client instance that was never started while assuming state is shared; start failure leaving a half-constructed engine reachable.","solutions":["Only call Expose after Start has returned nil, sequentially.","If the error persists after a successful Start, restart the client to get a cleanly started engine.","Guard the call site: check client.Status() or your own started flag before Expose."],"exampleFix":"// before\ngo func() { _ = client.Start(ctx) }()\nsess, err := client.Expose(ctx, req) // races engine start\n\n// after\nif err := client.Start(ctx); err != nil { return err }\nsess, err := client.Expose(ctx, req)","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"func canExpose(c *embed.Client) error {\n    // getEngine-equivalent guard: client must be started and engine up\n    if _, err := c.Status(); err != nil {\n        return fmt.Errorf(\"client not ready: %w\", err)\n    }\n    return nil\n}","tryCatchPattern":"sess, err := client.Expose(ctx, req)\nif err != nil {\n    if strings.Contains(err.Error(), \"expose manager not available\") {\n        // engine not started: call Start first, then retry once\n    }\n}","preventionTips":["Sequence Expose strictly after a successful Start in the same goroutine or via a ready gate.","Do not share Expose calls across Client instances; only the started one has a manager."],"tags":["expose","lifecycle","proxy","embed"],"backgroundTag":null,"analyzedSha":"93e97f4bf1ad715072dcb3fb6cdb1763431b5a9c","analyzedAt":"2026-08-16T03:09:19.136Z","schemaVersion":2},"datasetVersion":"2026-08-16T08:17:34.114Z"}