netbirdio/netbird · error

expose manager not available

Error message

expose manager not available

What it means

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.

Source

Thrown at client/embed/embed.go:433

		DialContext: c.Dial,
	}

	return &http.Client{
		Transport: transport,
	}
}

// Expose exposes a local service via the NetBird reverse proxy, making it accessible through a public URL.
// It returns an ExposeSession. Call Wait on the session to keep it alive.
func (c *Client) Expose(ctx context.Context, req ExposeRequest) (*ExposeSession, error) {
	engine, err := c.getEngine()
	if err != nil {
		return nil, err
	}

	mgr := engine.GetExposeManager()
	if mgr == nil {
		return nil, fmt.Errorf("expose manager not available")
	}

	resp, err := mgr.Expose(ctx, req)
	if err != nil {
		return nil, fmt.Errorf("expose: %w", err)
	}

	return &ExposeSession{
		Domain:      resp.Domain,
		ServiceName: resp.ServiceName,
		ServiceURL:  resp.ServiceURL,
		mgr:         mgr,
	}, nil
}

// IdentityForIP looks up a remote peer by its tunnel IP using the
// embedded client's status recorder. Returns the peer's WireGuard public
// key and FQDN. ok=false means the IP doesn't belong to an active peer

View on GitHub (pinned to 93e97f4bf1)

Solutions

  1. Only call Expose after Start has returned nil, sequentially.
  2. If the error persists after a successful Start, restart the client to get a cleanly started engine.
  3. Guard the call site: check client.Status() or your own started flag before Expose.

Example fix

// before
go func() { _ = client.Start(ctx) }()
sess, err := client.Expose(ctx, req) // races engine start

// after
if err := client.Start(ctx); err != nil { return err }
sess, err := client.Expose(ctx, req)
Defensive patterns

Strategy: type-guard

Type guard

func canExpose(c *embed.Client) error {
    // getEngine-equivalent guard: client must be started and engine up
    if _, err := c.Status(); err != nil {
        return fmt.Errorf("client not ready: %w", err)
    }
    return nil
}

Try / catch

sess, err := client.Expose(ctx, req)
if err != nil {
    if strings.Contains(err.Error(), "expose manager not available") {
        // engine not started: call Start first, then retry once
    }
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of netbirdio/netbird@93e97f4bf1 (2026-08-16). Data as JSON: /api/errors/38d4d15a32f4d2bd. Report an issue: GitHub.