wavetermdev/waveterm · error

cannot unbind %q to NoLinkId

Error message

cannot unbind %q to NoLinkId

What it means

unbindRouteLocally removes a routeId->linkId mapping from the router's routeMap. A linkId of baseds.NoLinkId (0) is not a real link, so attempting to unbind a route 'to' NoLinkId indicates a caller bug — there is no link to unbind from — and the call is rejected before touching the map.

Source

Thrown at pkg/wshutil/wshrouter.go:741

	if lm != nil {
		log.Printf("wshrouter unregister link %s", lm.Name())
	}
	delete(router.linkMap, linkId)
	if router.upstreamLinkId == linkId {
		router.upstreamLinkId = baseds.NoLinkId
	}
}

func isBindableRouteId(routeId string) bool {
	if routeId == "" || strings.HasPrefix(routeId, ControlPrefix) || strings.HasPrefix(routeId, RoutePrefix_Link) {
		return false
	}
	return true
}

func (router *WshRouter) unbindRouteLocally(linkId baseds.LinkId, routeId string) error {
	if linkId == baseds.NoLinkId {
		return fmt.Errorf("cannot unbind %q to NoLinkId", routeId)
	}
	router.lock.Lock()
	defer router.lock.Unlock()
	if router.routeMap[routeId] == linkId {
		delete(router.routeMap, routeId)
	}
	return nil
}

func (router *WshRouter) unbindRoute(linkId baseds.LinkId, routeId string) error {
	err := router.unbindRouteLocally(linkId, routeId)
	if err != nil {
		return err
	}
	lm := router.getLinkMeta(linkId)
	if lm != nil {
		log.Printf("wshrouter unbind route %q from %s", routeId, lm.Name())
	}

View on GitHub (pinned to a4447c1563)

Solutions

  1. Check the linkId before calling unbindRoute; skip the unbind when linkId == baseds.NoLinkId
  2. Fix the upstream lookup that produced NoLinkId (e.g. ensure RegisterUntrustedLink/RegisterRouterLink ran before unbind)
  3. If unbinding by route name, resolve the current routeMap entry first and only unbind when a real link id exists

Example fix

// before
err := router.unbindRoute(linkId, routeId)
// after
if linkId == baseds.NoLinkId {
    return nil // nothing bound; skip unbind
}
err := router.unbindRoute(linkId, routeId)
Defensive patterns

Strategy: validation

Validate before calling

if linkId == baseds.NoLinkId {
    return nil // or skip unbind
}
err := router.unbindRoute(linkId, routeId)

Type guard

func hasLink(id baseds.LinkId) bool { return id != baseds.NoLinkId }
if !hasLink(linkId) { return }

Try / catch

err := router.unbindRoute(linkId, routeId)
if err != nil && strings.Contains(err.Error(), "NoLinkId") {
    // link was never registered; treat as no-op or re-resolve linkId
}

Prevention

When it happens

Trigger: Calling WshRouter.unbindRoute (or unbindRouteLocally) with linkId == baseds.NoLinkId, typically because a lookup like getLinkMeta/routeMap lookup returned NoLinkId and its result was passed through unchecked.

Common situations: Code that resolves a link id from a route or upstream value that was never registered (upstreamLinkId reset to NoLinkId after UnregisterLink); passing a zero-valued struct field as linkId; calling unbind during shutdown when the link was already unregistered.

Related errors


AI-assisted analysis of wavetermdev/waveterm@a4447c1563 (2026-09-01). Data as JSON: /api/errors/1163d486d4c09445. Report an issue: GitHub.