halo-dev/halo · error · Error
Invalid attachment
Error message
Invalid attachment
What it means
Thrown by PluginServiceImpl.checkDependencies during plugin install() or upgrade() when pf4j's DependencyResolver detects a cycle in the dependency graph formed by the candidate plugin plus every currently resolved plugin. It is a PluginDependencyException.CyclicException (subclass of ServerWebInputException, HTTP 400) and sets the RFC 7807 problem type https://halo.run/probs/plugin-cyclic-dependency.
Source
Thrown at ui/packages/shared/src/utils/attachment.ts:113
* utils.attachment.getUrl(attachmentObject);
* // Returns: attachmentObject.status?.permalink
*
* // AttachmentSimple object
* utils.attachment.getUrl({ url: "https://example.com/image.jpg" });
* // Returns: "https://example.com/image.jpg"
* ```
*/
getUrl(attachment: AttachmentLike) {
if (typeof attachment === "string") {
return attachment;
}
if ("spec" in attachment) {
return attachment.status?.permalink;
}
if ("url" in attachment) {
return attachment.url;
}
throw new Error("Invalid attachment");
}
/**
* Converts an attachment-like object to a simplified attachment format
*
* @param attachment - The attachment object to convert (can be a string URL, Attachment object, or AttachmentSimple)
* @returns A simplified attachment object with url, alt, and mediaType properties, or undefined
* @throws {Error} When the attachment type is invalid or unrecognized
*
* @remarks
* This method normalizes different attachment formats into a consistent AttachmentSimple structure:
* 1. String: Converts to object with only url property
* 2. Attachment object (with "spec"): Extracts permalink, displayName, and mediaType
* 3. AttachmentSimple (with "url"): Returns as-is since it's already in the correct format
*
* @example
* ```ts
* import { utils } from "@halo-dev/ui-shared"View on GitHub (pinned to d2f5165f9c)
Solutions
- Open the failing plugin's plugin.yaml and remove the dependency edge that closes the cycle so the graph is a DAG.
- Make dependency direction one-way: if A declares B in pluginDependencies, ensure B does not declare A.
- Install leaf dependencies first and dependents last, in topological order.
- Inspect the resolved-plugin set too — the cycle may involve an already-installed plugin, not just the new one, so uninstall/repackage the offending side.
Example fix
// plugin.yaml (before) — cycle
spec:
pluginDependencies:
plugin-b: "1.0.0"
// plugin.yaml (after) — acyclic
spec:
pluginDependencies: {} Defensive patterns
Strategy: validation
Validate before calling
// Before install/upgrade, build the same descriptor set Halo uses and pre-check for cycles.
List<PluginDescriptor> descriptors = new ArrayList<>();
pluginManager.getResolvedPlugins().forEach(p -> descriptors.add(p.getDescriptor()));
descriptors.add(YamlPluginDescriptorFinder.convert(candidate));
DependencyResolver resolver = new DependencyResolver(pluginManager.getVersionManager());
DependencyResolver.Result r = resolver.resolve(descriptors);
if (r.hasCyclicDependency()) {
// refuse to upload; report which plugins form the cycle
} Try / catch
// install() returns Mono<Plugin>; react to PluginDependencyException.CyclicException
pluginService.install(path)
.onErrorResume(PluginDependencyException.CyclicException.class,
e -> Mono.fromRunnable(() -> log.warn("Refuse upload: cyclic dependency"))
.then(Mono.error(e))) Prevention
- Keep pluginDependencies a strict DAG in every published plugin.yaml.
- Run a dependency-cycle check in your plugin release CI before publishing.
- Document each dependency's metadata.name exactly to avoid edges to the wrong plugin.
When it happens
Trigger: Calling PluginService.install(Path) or PluginService.upgrade(name, Path) with a plugin whose spec.pluginDependencies, combined with the already-installed plugins, closes a loop (e.g. A depends on B and B depends on A, or a plugin lists itself).
Common situations: Two plugins that mutually reference each other; refactoring a plugin to depend on a downstream plugin that already depends on it; installing a plugin whose dependency is enabled but transitively points back.
Related errors
- User permissions not set in PermissionUtils
- ESM provider manifest must be an object.
- ESM provider manifest must contain format, entry, and option
- Host runtime snapshot haloVersion must be stable semver.
- Host runtime snapshot must expose exactly: ${SHARED_PACKAGE_
AI-assisted analysis of halo-dev/halo@d2f5165f9c (2026-08-14).
Data as JSON: /api/errors/0604cdebe3cb3c0c.
Report an issue: GitHub.