{"record":{"id":"e74bee202f848882","repo":"HumanSignal/label-studio","slug":"can-t-find-project-by-invite-url-url","errorCode":null,"errorMessage":"Can't find Project by invite URL: {url}","messagePattern":"Can't find Project by invite URL: (.+?)","errorType":"exception","errorClass":"KeyError","httpStatus":null,"severity":"error","filePath":"label_studio/projects/models.py","lineNumber":473,"sourceCode":"        if self.tasks.count() == 0:\n            return 0\n        return self.tasks.aggregate(Sum('overlap'))['overlap__sum']\n\n    @property\n    def get_available_for_labeling(self):\n        return self.get_collected_count - self.get_labeled_count\n\n    @property\n    def need_annotators(self):\n        return self.maximum_annotations - self.num_annotators\n\n    @classmethod\n    def find_by_invite_url(cls, url):\n        token = url.strip('/').split('/')[-1]\n        if len(token):\n            return Project.objects.get(token=token)\n        else:\n            raise KeyError(f\"Can't find Project by invite URL: {url}\")\n\n    def reset_token(self):\n        self.token = create_hash()\n        self.save(update_fields=['token'])\n\n    def add_collaborator(self, user):\n        created = False\n        with transaction.atomic():\n            try:\n                ProjectMember.objects.get(user=user, project=self)\n            except ProjectMember.DoesNotExist:\n                ProjectMember.objects.create(user=user, project=self)\n                created = True\n            else:\n                logger.debug(f'Project membership {self} for user {user} already exists')\n        return created\n\n    def has_collaborator(self, user):","sourceCodeStart":455,"sourceCodeEnd":491,"githubUrl":"https://github.com/HumanSignal/label-studio/blob/0b49e9b53917880baf1dd85d574fe5541a9aafb2/label_studio/projects/models.py#L455-L491","documentation":"Project.find_by_invite_url extracts the last path segment of an invite URL as a token; it raises KeyError only when the token is empty (URL has no trailing segment). A non-empty but unknown token will instead raise Project.DoesNotExist from .get().","triggerScenarios":"Calling Project.find_by_invite_url with '', a URL like 'https://host/p/invite' with nothing after the last '/', or a URL whose token was stripped before this call.","commonSituations":"Blank invite-link env/settings; copying base link without token; client trimming the URL; template rendering the token as empty because project.token was never generated (call reset_token()).","solutions":["Ensure the invite URL ends with the project's token","Regenerate the link via the project's reset_token / invite UI","Check Project.token is non-empty in the DB","Catch Project.DoesNotExist separately — the message shown would differ"],"exampleFix":"# before\nproject = Project.find_by_invite_url(invite_url)\n# after\nif not invite_url or not invite_url.strip('/').split('/')[-1]:\n    raise ValueError('Invite URL missing project token')\nproject = Project.find_by_invite_url(invite_url)","handlingStrategy":"validation","validationCode":"token = url.strip('/').split('/')[-1] if url else ''\nif not token:\n    raise ValueError('invite URL missing project token')","typeGuard":null,"tryCatchPattern":"try:\n    project = Project.find_by_invite_url(url)\nexcept KeyError:\n    ...  # empty token — fix link\nexcept Project.DoesNotExist:\n    ...  # unknown/expired token — regenerate link","preventionTips":["Call project.reset_token() if token is empty before sharing links","Do not trim trailing slashes/path in invite URLs","Validate invite link templates in settings at startup","Catch both KeyError and DoesNotExist when parsing user-supplied links"],"tags":["django","invite-link","url-parsing"],"backgroundTag":"invalid-invite-url","analyzedSha":"0b49e9b53917880baf1dd85d574fe5541a9aafb2","analyzedAt":"2026-08-29T00:39:52.578Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}