different-ai/openwork · error
Failed to verify skill plugin (${membershipResult.response.s
Error message
Failed to verify skill plugin (${membershipResult.response.status}). What it means
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.
Source
Thrown at ee/apps/den-web/app/(den)/dashboard/_components/skill-data.tsx:95
export function useSkill(pluginId: string, skillId: string) {
const { orgId } = useOrgDashboard();
const organizationId = orgId ?? "none";
return useQuery({
enabled: Boolean(orgId && pluginId && skillId),
queryKey: skillQueryKeys.detail(organizationId, pluginId, skillId),
queryFn: async (): Promise<DenSkill> => {
const encodedSkillId = encodeURIComponent(skillId);
const [{ response, payload }, membershipResult] = await Promise.all([
requestJson(`/v1/config-objects/${encodedSkillId}`, { method: "GET" }, 15000),
requestJson(`/v1/config-objects/${encodedSkillId}/plugins`, { method: "GET" }, 15000),
]);
if (!response.ok) {
throw new Error(getErrorMessage(payload, `Failed to load skill (${response.status}).`));
}
if (!membershipResult.response.ok) {
throw new Error(getErrorMessage(membershipResult.payload, `Failed to verify skill plugin (${membershipResult.response.status}).`));
}
const belongsToPlugin = isRecord(membershipResult.payload)
&& Array.isArray(membershipResult.payload.items)
&& membershipResult.payload.items.some((entry) => (
isRecord(entry) && entry.pluginId === pluginId && entry.removedAt === null
));
if (!belongsToPlugin) {
throw new Error("That skill is not part of this plugin.");
}
const skill = parseSkillResponse(payload);
if (!skill) {
throw new Error("Skill detail response was incomplete.");
}
return skill;
},
});
}
View on GitHub (pinned to 2b7df46e8a)
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
Example fix
// before
if (!membershipResult.response.ok) {
throw new Error(getErrorMessage(membershipResult.payload, `Failed to verify skill plugin (${membershipResult.response.status}).`));
}
// after
if (!membershipResult.response.ok) {
console.warn(`Skill plugin membership check failed (${membershipResult.response.status}); assuming no plugin.`);
return { skill: parseConfigObject(payload), belongsToPlugin: false };
} Defensive patterns
Strategy: fallback
Validate before calling
// probe the membership endpoint non-fatally before composing the query
async function fetchMembership(skillId: string): Promise<Response | null> {
const res = await fetch(`/v1/config-objects/${encodeURIComponent(skillId)}/plugins`);
return res.ok ? res : null; // caller degrades gracefully when null
} Type guard
function isMembershipList(payload: unknown): payload is { items: Array<{ pluginId: string; removedAt: string | null }> } {
return typeof payload === "object" && payload !== null && Array.isArray((payload as { items?: unknown }).items);
} Try / catch
try {
const [{ response, payload }, membershipResult] = await Promise.all([
requestJson(`/v1/config-objects/${encodedSkillId}`, { method: "GET" }, 15000),
requestJson(`/v1/config-objects/${encodedSkillId}/plugins`, { method: "GET" }, 15000).catch(() => null),
]);
if (!response.ok) throw new Error(getErrorMessage(payload, `Failed to load skill (${response.status}).`));
const belongsToPlugin = membershipResult && membershipResult.response.ok && isMembershipList(membershipResult.payload)
? membershipResult.payload.items.some((e) => e.pluginId === pluginId && e.removedAt === null)
: false;
} catch (error) {
return { error: error instanceof Error ? error.message : "Failed to load skill." };
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: 'Something went wrong' / 'Request failed (500)' / 'HTTP error! status: 404' — what failed HTTP requests actually mean and how to find the real cause — this error's family across 28 libraries.
Related errors
- Failed to load dashboards (${response.status}).
- Failed to load the dashboard (${response.status}).
- Failed to load dashboard access (${response.status}).
- Failed to load skill (${response.status}).
- Failed to load Workflow (${response.status}).
AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01).
Data as JSON: /api/errors/0af17056941f7ca0.
Report an issue: GitHub.