{"record":{"id":"1163d486d4c09445","repo":"wavetermdev/waveterm","slug":"cannot-unbind-q-to-nolinkid","errorCode":null,"errorMessage":"cannot unbind %q to NoLinkId","messagePattern":"cannot unbind %q to NoLinkId","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/wshutil/wshrouter.go","lineNumber":741,"sourceCode":"\tif lm != nil {\n\t\tlog.Printf(\"wshrouter unregister link %s\", lm.Name())\n\t}\n\tdelete(router.linkMap, linkId)\n\tif router.upstreamLinkId == linkId {\n\t\trouter.upstreamLinkId = baseds.NoLinkId\n\t}\n}\n\nfunc isBindableRouteId(routeId string) bool {\n\tif routeId == \"\" || strings.HasPrefix(routeId, ControlPrefix) || strings.HasPrefix(routeId, RoutePrefix_Link) {\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc (router *WshRouter) unbindRouteLocally(linkId baseds.LinkId, routeId string) error {\n\tif linkId == baseds.NoLinkId {\n\t\treturn fmt.Errorf(\"cannot unbind %q to NoLinkId\", routeId)\n\t}\n\trouter.lock.Lock()\n\tdefer router.lock.Unlock()\n\tif router.routeMap[routeId] == linkId {\n\t\tdelete(router.routeMap, routeId)\n\t}\n\treturn nil\n}\n\nfunc (router *WshRouter) unbindRoute(linkId baseds.LinkId, routeId string) error {\n\terr := router.unbindRouteLocally(linkId, routeId)\n\tif err != nil {\n\t\treturn err\n\t}\n\tlm := router.getLinkMeta(linkId)\n\tif lm != nil {\n\t\tlog.Printf(\"wshrouter unbind route %q from %s\", routeId, lm.Name())\n\t}","sourceCodeStart":723,"sourceCodeEnd":759,"githubUrl":"https://github.com/wavetermdev/waveterm/blob/a4447c1563b2df285ab89e76c82f91e1a1a49c1e/pkg/wshutil/wshrouter.go#L723-L759","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Check the linkId before calling unbindRoute; skip the unbind when linkId == baseds.NoLinkId","Fix the upstream lookup that produced NoLinkId (e.g. ensure RegisterUntrustedLink/RegisterRouterLink ran before unbind)","If unbinding by route name, resolve the current routeMap entry first and only unbind when a real link id exists"],"exampleFix":"// before\nerr := router.unbindRoute(linkId, routeId)\n// after\nif linkId == baseds.NoLinkId {\n    return nil // nothing bound; skip unbind\n}\nerr := router.unbindRoute(linkId, routeId)","handlingStrategy":"validation","validationCode":"if linkId == baseds.NoLinkId {\n    return nil // or skip unbind\n}\nerr := router.unbindRoute(linkId, routeId)","typeGuard":"func hasLink(id baseds.LinkId) bool { return id != baseds.NoLinkId }\nif !hasLink(linkId) { return }","tryCatchPattern":"err := router.unbindRoute(linkId, routeId)\nif err != nil && strings.Contains(err.Error(), \"NoLinkId\") {\n    // link was never registered; treat as no-op or re-resolve linkId\n}","preventionTips":["Resolve linkId from the router's own lookup right before unbinding","Skip unbind operations during teardown when links are already unregistered","Don't cache linkIds across reconnects"],"tags":["go","wshrouter","routing","invalid-argument"],"backgroundTag":"invalid-link-id","analyzedSha":"a4447c1563b2df285ab89e76c82f91e1a1a49c1e","analyzedAt":"2026-09-01T15:26:23.972Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}