{"record":{"id":"0af17056941f7ca0","repo":"different-ai/openwork","slug":"failed-to-verify-skill-plugin-membershipresult","errorCode":null,"errorMessage":"Failed to verify skill plugin (${membershipResult.response.status}).","messagePattern":"Failed to verify skill plugin \\((.+?)\\)\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"ee/apps/den-web/app/(den)/dashboard/_components/skill-data.tsx","lineNumber":95,"sourceCode":"\nexport function useSkill(pluginId: string, skillId: string) {\n  const { orgId } = useOrgDashboard();\n  const organizationId = orgId ?? \"none\";\n\n  return useQuery({\n    enabled: Boolean(orgId && pluginId && skillId),\n    queryKey: skillQueryKeys.detail(organizationId, pluginId, skillId),\n    queryFn: async (): Promise<DenSkill> => {\n      const encodedSkillId = encodeURIComponent(skillId);\n      const [{ response, payload }, membershipResult] = await Promise.all([\n        requestJson(`/v1/config-objects/${encodedSkillId}`, { method: \"GET\" }, 15000),\n        requestJson(`/v1/config-objects/${encodedSkillId}/plugins`, { method: \"GET\" }, 15000),\n      ]);\n      if (!response.ok) {\n        throw new Error(getErrorMessage(payload, `Failed to load skill (${response.status}).`));\n      }\n      if (!membershipResult.response.ok) {\n        throw new Error(getErrorMessage(membershipResult.payload, `Failed to verify skill plugin (${membershipResult.response.status}).`));\n      }\n      const belongsToPlugin = isRecord(membershipResult.payload)\n        && Array.isArray(membershipResult.payload.items)\n        && membershipResult.payload.items.some((entry) => (\n          isRecord(entry) && entry.pluginId === pluginId && entry.removedAt === null\n        ));\n      if (!belongsToPlugin) {\n        throw new Error(\"That skill is not part of this plugin.\");\n      }\n      const skill = parseSkillResponse(payload);\n      if (!skill) {\n        throw new Error(\"Skill detail response was incomplete.\");\n      }\n      return skill;\n    },\n  });\n}\n","sourceCodeStart":77,"sourceCodeEnd":113,"githubUrl":"https://github.com/different-ai/openwork/blob/2b7df46e8ae1517d64c896c7793d2d52ec845669/ee/apps/den-web/app/(den)/dashboard/_components/skill-data.tsx#L77-L113","documentation":"Thrown inside useSkill when the companion request GET /v1/config-objects/{id}/plugins (used to determine whether the skill belongs to a plugin) returns a non-ok status, even though the main skill fetch succeeded. The user sees the error state even though the skill data itself is available, because both requests are awaited via Promise.all and either failure aborts the query.","triggerScenarios":"GET /v1/config-objects/{encodedSkillId}/plugins responds 403 (endpoint not permitted for the user's role even though skill read is), 404 (route missing on an older API version), or 5xx — while the primary config-object GET is fine.","commonSituations":"Non-admin users lacking permission to list config-object plugin membership; API deployed without the /plugins sub-route (version skew); transient 5xx on the membership endpoint; wrong pluginId/org context in the request.","solutions":["Check the membership response status: 403 means permissions, 404 means route/version skew","Grant the calling role permission to read config-object plugin membership, or fall back to a less privileged membership endpoint","Deploy matching frontend and API versions so the /plugins route exists","Treat membership lookup as non-fatal: default belongsToPlugin to false and show a warning instead of failing the whole skill query"],"exampleFix":"// before\nif (!membershipResult.response.ok) {\n  throw new Error(getErrorMessage(membershipResult.payload, `Failed to verify skill plugin (${membershipResult.response.status}).`));\n}\n// after\nif (!membershipResult.response.ok) {\n  console.warn(`Skill plugin membership check failed (${membershipResult.response.status}); assuming no plugin.`);\n  return { skill: parseConfigObject(payload), belongsToPlugin: false };\n}","handlingStrategy":"fallback","validationCode":"// probe the membership endpoint non-fatally before composing the query\nasync function fetchMembership(skillId: string): Promise<Response | null> {\n  const res = await fetch(`/v1/config-objects/${encodeURIComponent(skillId)}/plugins`);\n  return res.ok ? res : null; // caller degrades gracefully when null\n}","typeGuard":"function isMembershipList(payload: unknown): payload is { items: Array<{ pluginId: string; removedAt: string | null }> } {\n  return typeof payload === \"object\" && payload !== null && Array.isArray((payload as { items?: unknown }).items);\n}","tryCatchPattern":"try {\n  const [{ response, payload }, membershipResult] = await Promise.all([\n    requestJson(`/v1/config-objects/${encodedSkillId}`, { method: \"GET\" }, 15000),\n    requestJson(`/v1/config-objects/${encodedSkillId}/plugins`, { method: \"GET\" }, 15000).catch(() => null),\n  ]);\n  if (!response.ok) throw new Error(getErrorMessage(payload, `Failed to load skill (${response.status}).`));\n  const belongsToPlugin = membershipResult && membershipResult.response.ok && isMembershipList(membershipResult.payload)\n    ? membershipResult.payload.items.some((e) => e.pluginId === pluginId && e.removedAt === null)\n    : false;\n} catch (error) {\n  return { error: error instanceof Error ? error.message : \"Failed to load skill.\" };\n}","preventionTips":["Treat secondary/enrichment fetches as optional and degrade to defaults instead of failing the page","Give all viewer roles read access to config-object membership, or expose a lighter endpoint","Add a contract test covering the /plugins membership route for non-admin users","Retry only transient statuses (429, 5xx); fail fast on 4xx with a clear message"],"tags":["network","http","permissions","react-query"],"backgroundTag":"http-request-failed","analyzedSha":"2b7df46e8ae1517d64c896c7793d2d52ec845669","analyzedAt":"2026-09-01T07:59:23.713Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}