different-ai/openwork · error

Failed to load plugin (${pluginResult.response.status}).

Error message

Failed to load plugin (${pluginResult.response.status}).

What it means

fetchResolvedPlugin issues two parallel GETs (/v1/plugins/:id and /v1/plugins/:id/resolved). If the plugin-detail request returns a non-2xx status it throws this error, using the server's error message from the payload when available. Callers (usePlugin, plugins queries) surface it via TanStack Query.

Source

Thrown at ee/apps/den-web/app/(den)/dashboard/_components/plugin-data.tsx:623

    case "SessionStart":
    case "SessionEnd":
    case "UserPromptSubmit":
    case "Notification":
    case "Stop":
      return value;
    default:
      return "Notification";
  }
}

async function fetchResolvedPlugin(id: string): Promise<DenPlugin | null> {
  const [pluginResult, membershipsResult] = await Promise.all([
    requestJson(`/v1/plugins/${encodeURIComponent(id)}`, { method: "GET" }, 15000),
    requestJson(`/v1/plugins/${encodeURIComponent(id)}/resolved`, { method: "GET" }, 15000),
  ]);

  if (!pluginResult.response.ok) {
    throw new Error(getErrorMessage(pluginResult.payload, `Failed to load plugin (${pluginResult.response.status}).`));
  }
  if (!membershipsResult.response.ok) {
    throw new Error(getErrorMessage(membershipsResult.payload, `Failed to load plugin contents (${membershipsResult.response.status}).`));
  }

  const pluginItem = isRecord(pluginResult.payload) && isRecord(pluginResult.payload.item) ? pluginResult.payload.item : null;
  if (!pluginItem) {
    return null;
  }

  const pluginId = asString(pluginItem.id);
  const name = asString(pluginItem.name);
  if (!pluginId || !name) {
    return null;
  }

  const membershipItems = isRecord(membershipsResult.payload) && Array.isArray(membershipsResult.payload.items)
    ? membershipsResult.payload.items.map(parseMembershipConfigObject).filter((value): value is NonNullable<typeof value> => Boolean(value))

View on GitHub (pinned to 2b7df46e8a)

Solutions

  1. Re-authenticate if status is 401/403; verify the session cookie is present in the failing request.
  2. Confirm the plugin id via the plugins list (GET /v1/plugins?status=active) and correct the URL/route.
  3. If the plugin was deleted, clear the stale link/query cache and navigate back to the plugin list.
  4. Check Den server logs/health when the status is 5xx; retry after the server recovers.

Example fix

// before: both requests fire even if the first fails
const [pluginResult, membershipsResult] = await Promise.all([...]);
if (!pluginResult.response.ok) {
  throw new Error(getErrorMessage(pluginResult.payload, `Failed to load plugin (${pluginResult.response.status}).`));
}
// after: short-circuit so a 404 doesn't also burn the memberships call
const pluginResult = await requestJson(`/v1/plugins/${encodeURIComponent(id)}`, { method: "GET" }, 15000);
if (!pluginResult.response.ok) {
  throw new Error(getErrorMessage(pluginResult.payload, `Failed to load plugin (${pluginResult.response.status}).`));
}
const membershipsResult = await requestJson(`/v1/plugins/${encodeURIComponent(id)}/resolved`, { method: "GET" }, 15000);
Defensive patterns

Strategy: try-catch

Validate before calling

const idOk = typeof id === "string" && id.length > 0;
const sessionOk = await ensureSession(); // redirect on 401 before issuing the request
if (!idOk || !sessionOk) return null;

Type guard

function isPluginPayload(p: unknown): p is { item: Record<string, unknown> } {
  return typeof p === "object" && p !== null && isRecord((p as { item?: unknown }).item);
}

Try / catch

try {
  const plugin = await fetchResolvedPlugin(id);
} catch (err) {
  const m = err instanceof Error ? err.message : "";
  if (m.includes("(404)")) { notFound(); return null; }
  if (m.includes("(401)") || m.includes("(403)")) { await redirectToSignIn(); return null; }
  showToast("Failed to load plugin; please retry.");
}

Prevention

When it happens

Trigger: GET /v1/plugins/{id} returns 401/403 (session expired, no permission on the plugin's org), 404 (plugin deleted or id mistyped), or 5xx from the Den server. Note both requests run unconditionally; the plugin fetch is checked first.

Common situations: Opening a bookmarked plugin route after the plugin was unpublished; stale pluginQueryKeys cache pointing at a deleted id; den-web pointed at a Den server in a different org/environment; server restart or migration in progress causing 5xx.

Related errors


AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01). Data as JSON: /api/errors/61e63ab40bcc433e. Report an issue: GitHub.